blob: b67200570a2045c85a3d890f5d90814fdcf306fe [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:
863 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
864 return self.is_gerrit
865
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000866 def GetSquashGerritUploads(self):
867 """Return true if uploads to Gerrit should be squashed by default."""
868 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700869 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
870 if self.squash_gerrit_uploads is None:
871 # Default is squash now (http://crbug.com/611892#c23).
872 self.squash_gerrit_uploads = not (
873 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
874 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000875 return self.squash_gerrit_uploads
876
tandriia60502f2016-06-20 02:01:53 -0700877 def GetSquashGerritUploadsOverride(self):
878 """Return True or False if codereview.settings should be overridden.
879
880 Returns None if no override has been defined.
881 """
882 # See also http://crbug.com/611892#c23
883 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
884 error_ok=True).strip()
885 if result == 'true':
886 return True
887 if result == 'false':
888 return False
889 return None
890
tandrii@chromium.org28253532016-04-14 13:46:56 +0000891 def GetGerritSkipEnsureAuthenticated(self):
892 """Return True if EnsureAuthenticated should not be done for Gerrit
893 uploads."""
894 if self.gerrit_skip_ensure_authenticated is None:
895 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000896 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000897 error_ok=True).strip() == 'true')
898 return self.gerrit_skip_ensure_authenticated
899
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000900 def GetGitEditor(self):
901 """Return the editor specified in the git config, or None if none is."""
902 if self.git_editor is None:
903 self.git_editor = self._GetConfig('core.editor', error_ok=True)
904 return self.git_editor or None
905
thestig@chromium.org44202a22014-03-11 19:22:18 +0000906 def GetLintRegex(self):
907 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
908 DEFAULT_LINT_REGEX)
909
910 def GetLintIgnoreRegex(self):
911 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
912 DEFAULT_LINT_IGNORE_REGEX)
913
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000914 def GetProject(self):
915 if not self.project:
916 self.project = self._GetRietveldConfig('project', error_ok=True)
917 return self.project
918
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000919 def _GetRietveldConfig(self, param, **kwargs):
920 return self._GetConfig('rietveld.' + param, **kwargs)
921
rmistry@google.com78948ed2015-07-08 23:09:57 +0000922 def _GetBranchConfig(self, branch_name, param, **kwargs):
923 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 def _GetConfig(self, param, **kwargs):
926 self.LazyUpdateIfNeeded()
927 return RunGit(['config', param], **kwargs).strip()
928
929
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100930@contextlib.contextmanager
931def _get_gerrit_project_config_file(remote_url):
932 """Context manager to fetch and store Gerrit's project.config from
933 refs/meta/config branch and store it in temp file.
934
935 Provides a temporary filename or None if there was error.
936 """
937 error, _ = RunGitWithCode([
938 'fetch', remote_url,
939 '+refs/meta/config:refs/git_cl/meta/config'])
940 if error:
941 # Ref doesn't exist or isn't accessible to current user.
942 print('WARNING: failed to fetch project config for %s: %s' %
943 (remote_url, error))
944 yield None
945 return
946
947 error, project_config_data = RunGitWithCode(
948 ['show', 'refs/git_cl/meta/config:project.config'])
949 if error:
950 print('WARNING: project.config file not found')
951 yield None
952 return
953
954 with gclient_utils.temporary_directory() as tempdir:
955 project_config_file = os.path.join(tempdir, 'project.config')
956 gclient_utils.FileWrite(project_config_file, project_config_data)
957 yield project_config_file
958
959
960def _is_git_numberer_enabled(remote_url, remote_ref):
961 """Returns True if Git Numberer is enabled on this ref."""
962 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100963 KNOWN_PROJECTS_WHITELIST = [
964 'chromium/src',
965 'external/webrtc',
966 'v8/v8',
967 ]
968
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100969 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
970 url_parts = urlparse.urlparse(remote_url)
971 project_name = url_parts.path.lstrip('/').rstrip('git./')
972 for known in KNOWN_PROJECTS_WHITELIST:
973 if project_name.endswith(known):
974 break
975 else:
976 # Early exit to avoid extra fetches for repos that aren't using Git
977 # Numberer.
978 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100979
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100980 with _get_gerrit_project_config_file(remote_url) as project_config_file:
981 if project_config_file is None:
982 # Failed to fetch project.config, which shouldn't happen on open source
983 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100984 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100985 def get_opts(x):
986 code, out = RunGitWithCode(
987 ['config', '-f', project_config_file, '--get-all',
988 'plugin.git-numberer.validate-%s-refglob' % x])
989 if code == 0:
990 return out.strip().splitlines()
991 return []
992 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100993
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100994 logging.info('validator config enabled %s disabled %s refglobs for '
995 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000996
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100997 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100998 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100999 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001000 return True
1001 return False
1002
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001003 if match_refglobs(disabled):
1004 return False
1005 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001006
1007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008def ShortBranchName(branch):
1009 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001010 return branch.replace('refs/heads/', '', 1)
1011
1012
1013def GetCurrentBranchRef():
1014 """Returns branch ref (e.g., refs/heads/master) or None."""
1015 return RunGit(['symbolic-ref', 'HEAD'],
1016 stderr=subprocess2.VOID, error_ok=True).strip() or None
1017
1018
1019def GetCurrentBranch():
1020 """Returns current branch or None.
1021
1022 For refs/heads/* branches, returns just last part. For others, full ref.
1023 """
1024 branchref = GetCurrentBranchRef()
1025 if branchref:
1026 return ShortBranchName(branchref)
1027 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028
1029
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001030class _CQState(object):
1031 """Enum for states of CL with respect to Commit Queue."""
1032 NONE = 'none'
1033 DRY_RUN = 'dry_run'
1034 COMMIT = 'commit'
1035
1036 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1037
1038
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001039class _ParsedIssueNumberArgument(object):
1040 def __init__(self, issue=None, patchset=None, hostname=None):
1041 self.issue = issue
1042 self.patchset = patchset
1043 self.hostname = hostname
1044
1045 @property
1046 def valid(self):
1047 return self.issue is not None
1048
1049
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050def ParseIssueNumberArgument(arg):
1051 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1052 fail_result = _ParsedIssueNumberArgument()
1053
1054 if arg.isdigit():
1055 return _ParsedIssueNumberArgument(issue=int(arg))
1056 if not arg.startswith('http'):
1057 return fail_result
1058 url = gclient_utils.UpgradeToHttps(arg)
1059 try:
1060 parsed_url = urlparse.urlparse(url)
1061 except ValueError:
1062 return fail_result
1063 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1064 tmp = cls.ParseIssueURL(parsed_url)
1065 if tmp is not None:
1066 return tmp
1067 return fail_result
1068
1069
Aaron Gablea45ee112016-11-22 15:14:38 -08001070class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001071 def __init__(self, issue, url):
1072 self.issue = issue
1073 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001074 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001075
1076 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001077 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001078 self.issue, self.url)
1079
1080
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001081_CommentSummary = collections.namedtuple(
1082 '_CommentSummary', ['date', 'message', 'sender',
1083 # TODO(tandrii): these two aren't known in Gerrit.
1084 'approval', 'disapproval'])
1085
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001088 """Changelist works with one changelist in local branch.
1089
1090 Supports two codereview backends: Rietveld or Gerrit, selected at object
1091 creation.
1092
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001093 Notes:
1094 * Not safe for concurrent multi-{thread,process} use.
1095 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001096 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001097 """
1098
1099 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1100 """Create a new ChangeList instance.
1101
1102 If issue is given, the codereview must be given too.
1103
1104 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1105 Otherwise, it's decided based on current configuration of the local branch,
1106 with default being 'rietveld' for backwards compatibility.
1107 See _load_codereview_impl for more details.
1108
1109 **kwargs will be passed directly to codereview implementation.
1110 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001112 global settings
1113 if not settings:
1114 # Happens when git_cl.py is used as a utility library.
1115 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116
1117 if issue:
1118 assert codereview, 'codereview must be known, if issue is known'
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 self.branchref = branchref
1121 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001122 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 self.branch = ShortBranchName(self.branchref)
1124 else:
1125 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 self.lookedup_issue = False
1128 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 self.has_description = False
1130 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001131 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001133 self.cc = None
1134 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001135 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001136
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001137 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001138 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001140 assert self._codereview_impl
1141 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142
1143 def _load_codereview_impl(self, codereview=None, **kwargs):
1144 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1146 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1147 self._codereview = codereview
1148 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149 return
1150
1151 # Automatic selection based on issue number set for a current branch.
1152 # Rietveld takes precedence over Gerrit.
1153 assert not self.issue
1154 # Whether we find issue or not, we are doing the lookup.
1155 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001156 if self.GetBranch():
1157 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1158 issue = _git_get_branch_config_value(
1159 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1160 if issue:
1161 self._codereview = codereview
1162 self._codereview_impl = cls(self, **kwargs)
1163 self.issue = int(issue)
1164 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001165
1166 # No issue is set for this branch, so decide based on repo-wide settings.
1167 return self._load_codereview_impl(
1168 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1169 **kwargs)
1170
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001171 def IsGerrit(self):
1172 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001173
1174 def GetCCList(self):
1175 """Return the users cc'd on this CL.
1176
agable92bec4f2016-08-24 09:27:27 -07001177 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001178 """
1179 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001180 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001181 more_cc = ','.join(self.watchers)
1182 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1183 return self.cc
1184
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001185 def GetCCListWithoutDefault(self):
1186 """Return the users cc'd on this CL excluding default ones."""
1187 if self.cc is None:
1188 self.cc = ','.join(self.watchers)
1189 return self.cc
1190
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001191 def SetWatchers(self, watchers):
1192 """Set the list of email addresses that should be cc'd based on the changed
1193 files in this CL.
1194 """
1195 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196
1197 def GetBranch(self):
1198 """Returns the short branch name, e.g. 'master'."""
1199 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001200 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001201 if not branchref:
1202 return None
1203 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 self.branch = ShortBranchName(self.branchref)
1205 return self.branch
1206
1207 def GetBranchRef(self):
1208 """Returns the full branch name, e.g. 'refs/heads/master'."""
1209 self.GetBranch() # Poke the lazy loader.
1210 return self.branchref
1211
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001212 def ClearBranch(self):
1213 """Clears cached branch data of this object."""
1214 self.branch = self.branchref = None
1215
tandrii5d48c322016-08-18 16:19:37 -07001216 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1217 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1218 kwargs['branch'] = self.GetBranch()
1219 return _git_get_branch_config_value(key, default, **kwargs)
1220
1221 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1222 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1223 assert self.GetBranch(), (
1224 'this CL must have an associated branch to %sset %s%s' %
1225 ('un' if value is None else '',
1226 key,
1227 '' if value is None else ' to %r' % value))
1228 kwargs['branch'] = self.GetBranch()
1229 return _git_set_branch_config_value(key, value, **kwargs)
1230
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 @staticmethod
1232 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001233 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 e.g. 'origin', 'refs/heads/master'
1235 """
1236 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001237 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1238
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001240 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001242 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1243 error_ok=True).strip()
1244 if upstream_branch:
1245 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001247 # Else, try to guess the origin remote.
1248 remote_branches = RunGit(['branch', '-r']).split()
1249 if 'origin/master' in remote_branches:
1250 # Fall back on origin/master if it exits.
1251 remote = 'origin'
1252 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001254 DieWithError(
1255 'Unable to determine default branch to diff against.\n'
1256 'Either pass complete "git diff"-style arguments, like\n'
1257 ' git cl upload origin/master\n'
1258 'or verify this branch is set up to track another \n'
1259 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260
1261 return remote, upstream_branch
1262
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001263 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001264 upstream_branch = self.GetUpstreamBranch()
1265 if not BranchExists(upstream_branch):
1266 DieWithError('The upstream for the current branch (%s) does not exist '
1267 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001268 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001269 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 def GetUpstreamBranch(self):
1272 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001275 upstream_branch = upstream_branch.replace('refs/heads/',
1276 'refs/remotes/%s/' % remote)
1277 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1278 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 self.upstream_branch = upstream_branch
1280 return self.upstream_branch
1281
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001282 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001283 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 remote, branch = None, self.GetBranch()
1285 seen_branches = set()
1286 while branch not in seen_branches:
1287 seen_branches.add(branch)
1288 remote, branch = self.FetchUpstreamTuple(branch)
1289 branch = ShortBranchName(branch)
1290 if remote != '.' or branch.startswith('refs/remotes'):
1291 break
1292 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001293 remotes = RunGit(['remote'], error_ok=True).split()
1294 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001295 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001296 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001297 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001298 logging.warn('Could not determine which remote this change is '
1299 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001300 else:
1301 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001302 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 branch = 'HEAD'
1304 if branch.startswith('refs/remotes'):
1305 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001306 elif branch.startswith('refs/branch-heads/'):
1307 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 else:
1309 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 return self._remote
1311
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 def GitSanityChecks(self, upstream_git_obj):
1313 """Checks git repo status and ensures diff is from local commits."""
1314
sbc@chromium.org79706062015-01-14 21:18:12 +00001315 if upstream_git_obj is None:
1316 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001317 print('ERROR: unable to determine current branch (detached HEAD?)',
1318 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001319 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001320 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001321 return False
1322
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 # Verify the commit we're diffing against is in our current branch.
1324 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1325 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1326 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001327 print('ERROR: %s is not in the current branch. You may need to rebase '
1328 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 return False
1330
1331 # List the commits inside the diff, and verify they are all local.
1332 commits_in_diff = RunGit(
1333 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1334 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1335 remote_branch = remote_branch.strip()
1336 if code != 0:
1337 _, remote_branch = self.GetRemoteBranch()
1338
1339 commits_in_remote = RunGit(
1340 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1341
1342 common_commits = set(commits_in_diff) & set(commits_in_remote)
1343 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001344 print('ERROR: Your diff contains %d commits already in %s.\n'
1345 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1346 'the diff. If you are using a custom git flow, you can override'
1347 ' the reference used for this check with "git config '
1348 'gitcl.remotebranch <git-ref>".' % (
1349 len(common_commits), remote_branch, upstream_git_obj),
1350 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 return False
1352 return True
1353
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001354 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001355 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001356
1357 Returns None if it is not set.
1358 """
tandrii5d48c322016-08-18 16:19:37 -07001359 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001360
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 def GetRemoteUrl(self):
1362 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1363
1364 Returns None if there is no remote.
1365 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001366 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001367 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1368
1369 # If URL is pointing to a local directory, it is probably a git cache.
1370 if os.path.isdir(url):
1371 url = RunGit(['config', 'remote.%s.url' % remote],
1372 error_ok=True,
1373 cwd=url).strip()
1374 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001376 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001377 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001378 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001379 self.issue = self._GitGetBranchConfigValue(
1380 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001381 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return self.issue
1383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 def GetIssueURL(self):
1385 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001386 issue = self.GetIssue()
1387 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001388 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001389 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001391 def GetDescription(self, pretty=False, force=False):
1392 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001394 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395 self.has_description = True
1396 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001397 # Set width to 72 columns + 2 space indent.
1398 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001400 lines = self.description.splitlines()
1401 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 return self.description
1403
1404 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001405 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001406 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001407 self.patchset = self._GitGetBranchConfigValue(
1408 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 return self.patchset
1411
1412 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001413 """Set this branch's patchset. If patchset=0, clears the patchset."""
1414 assert self.GetBranch()
1415 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001416 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001417 else:
1418 self.patchset = int(patchset)
1419 self._GitSetBranchConfigValue(
1420 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001422 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001423 """Set this branch's issue. If issue isn't given, clears the issue."""
1424 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001426 issue = int(issue)
1427 self._GitSetBranchConfigValue(
1428 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001429 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001430 codereview_server = self._codereview_impl.GetCodereviewServer()
1431 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001432 self._GitSetBranchConfigValue(
1433 self._codereview_impl.CodereviewServerConfigKey(),
1434 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 else:
tandrii5d48c322016-08-18 16:19:37 -07001436 # Reset all of these just to be clean.
1437 reset_suffixes = [
1438 'last-upload-hash',
1439 self._codereview_impl.IssueConfigKey(),
1440 self._codereview_impl.PatchsetConfigKey(),
1441 self._codereview_impl.CodereviewServerConfigKey(),
1442 ] + self._PostUnsetIssueProperties()
1443 for prop in reset_suffixes:
1444 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001446 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447
dnjba1b0f32016-09-02 12:37:42 -07001448 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 if not self.GitSanityChecks(upstream_branch):
1450 DieWithError('\nGit sanity check failure')
1451
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001452 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001453 if not root:
1454 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001456
1457 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001459 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001461 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001462 except subprocess2.CalledProcessError:
1463 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001464 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 'This branch probably doesn\'t exist anymore. To reset the\n'
1466 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001467 ' git branch --set-upstream-to origin/master %s\n'
1468 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470
maruel@chromium.org52424302012-08-29 15:14:30 +00001471 issue = self.GetIssue()
1472 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001473 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001474 description = self.GetDescription()
1475 else:
1476 # If the change was never uploaded, use the log messages of all commits
1477 # up to the branch point, as git cl upload will prefill the description
1478 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1480 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001481
1482 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001483 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001484 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001485 name,
1486 description,
1487 absroot,
1488 files,
1489 issue,
1490 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001491 author,
1492 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493
dsansomee2d6fd92016-09-08 00:10:47 -07001494 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001497 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498
1499 def RunHook(self, committing, may_prompt, verbose, change):
1500 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1501 try:
1502 return presubmit_support.DoPresubmitChecks(change, committing,
1503 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1504 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001505 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1506 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001507 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508 DieWithError(
1509 ('%s\nMaybe your depot_tools is out of date?\n'
1510 'If all fails, contact maruel@') % e)
1511
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001512 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1513 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001514 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1515 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001516 else:
1517 # Assume url.
1518 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1519 urlparse.urlparse(issue_arg))
1520 if not parsed_issue_arg or not parsed_issue_arg.valid:
1521 DieWithError('Failed to parse issue argument "%s". '
1522 'Must be an issue number or a valid URL.' % issue_arg)
1523 return self._codereview_impl.CMDPatchWithParsedIssue(
1524 parsed_issue_arg, reject, nocommit, directory)
1525
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001526 def CMDUpload(self, options, git_diff_args, orig_args):
1527 """Uploads a change to codereview."""
1528 if git_diff_args:
1529 # TODO(ukai): is it ok for gerrit case?
1530 base_branch = git_diff_args[0]
1531 else:
1532 if self.GetBranch() is None:
1533 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1534
1535 # Default to diffing against common ancestor of upstream branch
1536 base_branch = self.GetCommonAncestorWithUpstream()
1537 git_diff_args = [base_branch, 'HEAD']
1538
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001539 # Fast best-effort checks to abort before running potentially
1540 # expensive hooks if uploading is likely to fail anyway. Passing these
1541 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001542 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001543 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001544
1545 # Apply watchlists on upload.
1546 change = self.GetChange(base_branch, None)
1547 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1548 files = [f.LocalPath() for f in change.AffectedFiles()]
1549 if not options.bypass_watchlists:
1550 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1551
1552 if not options.bypass_hooks:
1553 if options.reviewers or options.tbr_owners:
1554 # Set the reviewer list now so that presubmit checks can access it.
1555 change_description = ChangeDescription(change.FullDescriptionText())
1556 change_description.update_reviewers(options.reviewers,
1557 options.tbr_owners,
1558 change)
1559 change.SetDescriptionText(change_description.description)
1560 hook_results = self.RunHook(committing=False,
1561 may_prompt=not options.force,
1562 verbose=options.verbose,
1563 change=change)
1564 if not hook_results.should_continue():
1565 return 1
1566 if not options.reviewers and hook_results.reviewers:
1567 options.reviewers = hook_results.reviewers.split(',')
1568
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001569 # TODO(tandrii): Checking local patchset against remote patchset is only
1570 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1571 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001572 latest_patchset = self.GetMostRecentPatchset()
1573 local_patchset = self.GetPatchset()
1574 if (latest_patchset and local_patchset and
1575 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001576 print('The last upload made from this repository was patchset #%d but '
1577 'the most recent patchset on the server is #%d.'
1578 % (local_patchset, latest_patchset))
1579 print('Uploading will still work, but if you\'ve uploaded to this '
1580 'issue from another machine or branch the patch you\'re '
1581 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001582 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583
1584 print_stats(options.similarity, options.find_copies, git_diff_args)
1585 ret = self.CMDUploadChange(options, git_diff_args, change)
1586 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001587 if options.use_commit_queue:
1588 self.SetCQState(_CQState.COMMIT)
1589 elif options.cq_dry_run:
1590 self.SetCQState(_CQState.DRY_RUN)
1591
tandrii5d48c322016-08-18 16:19:37 -07001592 _git_set_branch_config_value('last-upload-hash',
1593 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 # Run post upload hooks, if specified.
1595 if settings.GetRunPostUploadHook():
1596 presubmit_support.DoPostUploadExecuter(
1597 change,
1598 self,
1599 settings.GetRoot(),
1600 options.verbose,
1601 sys.stdout)
1602
1603 # Upload all dependencies if specified.
1604 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001605 print()
1606 print('--dependencies has been specified.')
1607 print('All dependent local branches will be re-uploaded.')
1608 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001609 # Remove the dependencies flag from args so that we do not end up in a
1610 # loop.
1611 orig_args.remove('--dependencies')
1612 ret = upload_branch_deps(self, orig_args)
1613 return ret
1614
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001615 def SetCQState(self, new_state):
1616 """Update the CQ state for latest patchset.
1617
1618 Issue must have been already uploaded and known.
1619 """
1620 assert new_state in _CQState.ALL_STATES
1621 assert self.GetIssue()
1622 return self._codereview_impl.SetCQState(new_state)
1623
qyearsley1fdfcb62016-10-24 13:22:03 -07001624 def TriggerDryRun(self):
1625 """Triggers a dry run and prints a warning on failure."""
1626 # TODO(qyearsley): Either re-use this method in CMDset_commit
1627 # and CMDupload, or change CMDtry to trigger dry runs with
1628 # just SetCQState, and catch keyboard interrupt and other
1629 # errors in that method.
1630 try:
1631 self.SetCQState(_CQState.DRY_RUN)
1632 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1633 return 0
1634 except KeyboardInterrupt:
1635 raise
1636 except:
1637 print('WARNING: failed to trigger CQ Dry Run.\n'
1638 'Either:\n'
1639 ' * your project has no CQ\n'
1640 ' * you don\'t have permission to trigger Dry Run\n'
1641 ' * bug in this code (see stack trace below).\n'
1642 'Consider specifying which bots to trigger manually '
1643 'or asking your project owners for permissions '
1644 'or contacting Chrome Infrastructure team at '
1645 'https://www.chromium.org/infra\n\n')
1646 # Still raise exception so that stack trace is printed.
1647 raise
1648
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 # Forward methods to codereview specific implementation.
1650
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001651 def AddComment(self, message):
1652 return self._codereview_impl.AddComment(message)
1653
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001654 def GetCommentsSummary(self):
1655 """Returns list of _CommentSummary for each comment.
1656
1657 Note: comments per file or per line are not included,
1658 only top-level comments are returned.
1659 """
1660 return self._codereview_impl.GetCommentsSummary()
1661
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001662 def CloseIssue(self):
1663 return self._codereview_impl.CloseIssue()
1664
1665 def GetStatus(self):
1666 return self._codereview_impl.GetStatus()
1667
1668 def GetCodereviewServer(self):
1669 return self._codereview_impl.GetCodereviewServer()
1670
tandriide281ae2016-10-12 06:02:30 -07001671 def GetIssueOwner(self):
1672 """Get owner from codereview, which may differ from this checkout."""
1673 return self._codereview_impl.GetIssueOwner()
1674
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001675 def GetApprovingReviewers(self):
1676 return self._codereview_impl.GetApprovingReviewers()
1677
1678 def GetMostRecentPatchset(self):
1679 return self._codereview_impl.GetMostRecentPatchset()
1680
tandriide281ae2016-10-12 06:02:30 -07001681 def CannotTriggerTryJobReason(self):
1682 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1683 return self._codereview_impl.CannotTriggerTryJobReason()
1684
tandrii8c5a3532016-11-04 07:52:02 -07001685 def GetTryjobProperties(self, patchset=None):
1686 """Returns dictionary of properties to launch tryjob."""
1687 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1688
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 def __getattr__(self, attr):
1690 # This is because lots of untested code accesses Rietveld-specific stuff
1691 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001692 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001693 # Note that child method defines __getattr__ as well, and forwards it here,
1694 # because _RietveldChangelistImpl is not cleaned up yet, and given
1695 # deprecation of Rietveld, it should probably be just removed.
1696 # Until that time, avoid infinite recursion by bypassing __getattr__
1697 # of implementation class.
1698 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001699
1700
1701class _ChangelistCodereviewBase(object):
1702 """Abstract base class encapsulating codereview specifics of a changelist."""
1703 def __init__(self, changelist):
1704 self._changelist = changelist # instance of Changelist
1705
1706 def __getattr__(self, attr):
1707 # Forward methods to changelist.
1708 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1709 # _RietveldChangelistImpl to avoid this hack?
1710 return getattr(self._changelist, attr)
1711
1712 def GetStatus(self):
1713 """Apply a rough heuristic to give a simple summary of an issue's review
1714 or CQ status, assuming adherence to a common workflow.
1715
1716 Returns None if no issue for this branch, or specific string keywords.
1717 """
1718 raise NotImplementedError()
1719
1720 def GetCodereviewServer(self):
1721 """Returns server URL without end slash, like "https://codereview.com"."""
1722 raise NotImplementedError()
1723
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001724 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 """Fetches and returns description from the codereview server."""
1726 raise NotImplementedError()
1727
tandrii5d48c322016-08-18 16:19:37 -07001728 @classmethod
1729 def IssueConfigKey(cls):
1730 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001731 raise NotImplementedError()
1732
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001733 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001734 def PatchsetConfigKey(cls):
1735 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 raise NotImplementedError()
1737
tandrii5d48c322016-08-18 16:19:37 -07001738 @classmethod
1739 def CodereviewServerConfigKey(cls):
1740 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 raise NotImplementedError()
1742
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001743 def _PostUnsetIssueProperties(self):
1744 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001745 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001746
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001747 def GetRieveldObjForPresubmit(self):
1748 # This is an unfortunate Rietveld-embeddedness in presubmit.
1749 # For non-Rietveld codereviews, this probably should return a dummy object.
1750 raise NotImplementedError()
1751
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001752 def GetGerritObjForPresubmit(self):
1753 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1754 return None
1755
dsansomee2d6fd92016-09-08 00:10:47 -07001756 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001757 """Update the description on codereview site."""
1758 raise NotImplementedError()
1759
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001760 def AddComment(self, message):
1761 """Posts a comment to the codereview site."""
1762 raise NotImplementedError()
1763
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001764 def GetCommentsSummary(self):
1765 raise NotImplementedError()
1766
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 def CloseIssue(self):
1768 """Closes the issue."""
1769 raise NotImplementedError()
1770
1771 def GetApprovingReviewers(self):
1772 """Returns a list of reviewers approving the change.
1773
1774 Note: not necessarily committers.
1775 """
1776 raise NotImplementedError()
1777
1778 def GetMostRecentPatchset(self):
1779 """Returns the most recent patchset number from the codereview site."""
1780 raise NotImplementedError()
1781
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001782 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1783 directory):
1784 """Fetches and applies the issue.
1785
1786 Arguments:
1787 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1788 reject: if True, reject the failed patch instead of switching to 3-way
1789 merge. Rietveld only.
1790 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1791 only.
1792 directory: switch to directory before applying the patch. Rietveld only.
1793 """
1794 raise NotImplementedError()
1795
1796 @staticmethod
1797 def ParseIssueURL(parsed_url):
1798 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1799 failed."""
1800 raise NotImplementedError()
1801
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001802 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001803 """Best effort check that user is authenticated with codereview server.
1804
1805 Arguments:
1806 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001807 refresh: whether to attempt to refresh credentials. Ignored if not
1808 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001809 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001810 raise NotImplementedError()
1811
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001812 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001813 """Best effort check that uploading isn't supposed to fail for predictable
1814 reasons.
1815
1816 This method should raise informative exception if uploading shouldn't
1817 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001818
1819 Arguments:
1820 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001821 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001822 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001823
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001824 def CMDUploadChange(self, options, args, change):
1825 """Uploads a change to codereview."""
1826 raise NotImplementedError()
1827
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001828 def SetCQState(self, new_state):
1829 """Update the CQ state for latest patchset.
1830
1831 Issue must have been already uploaded and known.
1832 """
1833 raise NotImplementedError()
1834
tandriie113dfd2016-10-11 10:20:12 -07001835 def CannotTriggerTryJobReason(self):
1836 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1837 raise NotImplementedError()
1838
tandriide281ae2016-10-12 06:02:30 -07001839 def GetIssueOwner(self):
1840 raise NotImplementedError()
1841
tandrii8c5a3532016-11-04 07:52:02 -07001842 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001843 raise NotImplementedError()
1844
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845
1846class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001847 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 super(_RietveldChangelistImpl, self).__init__(changelist)
1849 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001850 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001851 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001853 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001854 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001855 self._props = None
1856 self._rpc_server = None
1857
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 def GetCodereviewServer(self):
1859 if not self._rietveld_server:
1860 # If we're on a branch then get the server potentially associated
1861 # with that branch.
1862 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001863 self._rietveld_server = gclient_utils.UpgradeToHttps(
1864 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865 if not self._rietveld_server:
1866 self._rietveld_server = settings.GetDefaultServerUrl()
1867 return self._rietveld_server
1868
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001869 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001870 """Best effort check that user is authenticated with Rietveld server."""
1871 if self._auth_config.use_oauth2:
1872 authenticator = auth.get_authenticator_for_host(
1873 self.GetCodereviewServer(), self._auth_config)
1874 if not authenticator.has_cached_credentials():
1875 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001876 if refresh:
1877 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001878
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001879 def EnsureCanUploadPatchset(self, force):
1880 # No checks for Rietveld because we are deprecating Rietveld.
1881 pass
1882
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001883 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001884 issue = self.GetIssue()
1885 assert issue
1886 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001887 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001888 except urllib2.HTTPError as e:
1889 if e.code == 404:
1890 DieWithError(
1891 ('\nWhile fetching the description for issue %d, received a '
1892 '404 (not found)\n'
1893 'error. It is likely that you deleted this '
1894 'issue on the server. If this is the\n'
1895 'case, please run\n\n'
1896 ' git cl issue 0\n\n'
1897 'to clear the association with the deleted issue. Then run '
1898 'this command again.') % issue)
1899 else:
1900 DieWithError(
1901 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1902 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001903 print('Warning: Failed to retrieve CL description due to network '
1904 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 return ''
1906
1907 def GetMostRecentPatchset(self):
1908 return self.GetIssueProperties()['patchsets'][-1]
1909
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001910 def GetIssueProperties(self):
1911 if self._props is None:
1912 issue = self.GetIssue()
1913 if not issue:
1914 self._props = {}
1915 else:
1916 self._props = self.RpcServer().get_issue_properties(issue, True)
1917 return self._props
1918
tandriie113dfd2016-10-11 10:20:12 -07001919 def CannotTriggerTryJobReason(self):
1920 props = self.GetIssueProperties()
1921 if not props:
1922 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1923 if props.get('closed'):
1924 return 'CL %s is closed' % self.GetIssue()
1925 if props.get('private'):
1926 return 'CL %s is private' % self.GetIssue()
1927 return None
1928
tandrii8c5a3532016-11-04 07:52:02 -07001929 def GetTryjobProperties(self, patchset=None):
1930 """Returns dictionary of properties to launch tryjob."""
1931 project = (self.GetIssueProperties() or {}).get('project')
1932 return {
1933 'issue': self.GetIssue(),
1934 'patch_project': project,
1935 'patch_storage': 'rietveld',
1936 'patchset': patchset or self.GetPatchset(),
1937 'rietveld': self.GetCodereviewServer(),
1938 }
1939
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001940 def GetApprovingReviewers(self):
1941 return get_approving_reviewers(self.GetIssueProperties())
1942
tandriide281ae2016-10-12 06:02:30 -07001943 def GetIssueOwner(self):
1944 return (self.GetIssueProperties() or {}).get('owner_email')
1945
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001946 def AddComment(self, message):
1947 return self.RpcServer().add_comment(self.GetIssue(), message)
1948
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001949 def GetCommentsSummary(self):
1950 summary = []
1951 for message in self.GetIssueProperties().get('messages', []):
1952 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1953 summary.append(_CommentSummary(
1954 date=date,
1955 disapproval=bool(message['disapproval']),
1956 approval=bool(message['approval']),
1957 sender=message['sender'],
1958 message=message['text'],
1959 ))
1960 return summary
1961
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001962 def GetStatus(self):
1963 """Apply a rough heuristic to give a simple summary of an issue's review
1964 or CQ status, assuming adherence to a common workflow.
1965
1966 Returns None if no issue for this branch, or one of the following keywords:
1967 * 'error' - error from review tool (including deleted issues)
1968 * 'unsent' - not sent for review
1969 * 'waiting' - waiting for review
1970 * 'reply' - waiting for owner to reply to review
1971 * 'lgtm' - LGTM from at least one approved reviewer
1972 * 'commit' - in the commit queue
1973 * 'closed' - closed
1974 """
1975 if not self.GetIssue():
1976 return None
1977
1978 try:
1979 props = self.GetIssueProperties()
1980 except urllib2.HTTPError:
1981 return 'error'
1982
1983 if props.get('closed'):
1984 # Issue is closed.
1985 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001986 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001987 # Issue is in the commit queue.
1988 return 'commit'
1989
1990 try:
1991 reviewers = self.GetApprovingReviewers()
1992 except urllib2.HTTPError:
1993 return 'error'
1994
1995 if reviewers:
1996 # Was LGTM'ed.
1997 return 'lgtm'
1998
1999 messages = props.get('messages') or []
2000
tandrii9d2c7a32016-06-22 03:42:45 -07002001 # Skip CQ messages that don't require owner's action.
2002 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2003 if 'Dry run:' in messages[-1]['text']:
2004 messages.pop()
2005 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2006 # This message always follows prior messages from CQ,
2007 # so skip this too.
2008 messages.pop()
2009 else:
2010 # This is probably a CQ messages warranting user attention.
2011 break
2012
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002013 if not messages:
2014 # No message was sent.
2015 return 'unsent'
2016 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002017 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002018 return 'reply'
2019 return 'waiting'
2020
dsansomee2d6fd92016-09-08 00:10:47 -07002021 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002022 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002025 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002027 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002028 return self.SetFlags({flag: value})
2029
2030 def SetFlags(self, flags):
2031 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002032 """
phajdan.jr68598232016-08-10 03:28:28 -07002033 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002034 try:
tandrii4b233bd2016-07-06 03:50:29 -07002035 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002036 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002037 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002038 if e.code == 404:
2039 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2040 if e.code == 403:
2041 DieWithError(
2042 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002043 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002044 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002046 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047 """Returns an upload.RpcServer() to access this review's rietveld instance.
2048 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002049 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002050 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002051 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002052 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002053 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002055 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002056 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002057 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058
tandrii5d48c322016-08-18 16:19:37 -07002059 @classmethod
2060 def PatchsetConfigKey(cls):
2061 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062
tandrii5d48c322016-08-18 16:19:37 -07002063 @classmethod
2064 def CodereviewServerConfigKey(cls):
2065 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002066
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067 def GetRieveldObjForPresubmit(self):
2068 return self.RpcServer()
2069
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002070 def SetCQState(self, new_state):
2071 props = self.GetIssueProperties()
2072 if props.get('private'):
2073 DieWithError('Cannot set-commit on private issue')
2074
2075 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002076 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002077 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002078 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002079 else:
tandrii4b233bd2016-07-06 03:50:29 -07002080 assert new_state == _CQState.DRY_RUN
2081 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002082
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002083 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2084 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002085 # PatchIssue should never be called with a dirty tree. It is up to the
2086 # caller to check this, but just in case we assert here since the
2087 # consequences of the caller not checking this could be dire.
2088 assert(not git_common.is_dirty_git_tree('apply'))
2089 assert(parsed_issue_arg.valid)
2090 self._changelist.issue = parsed_issue_arg.issue
2091 if parsed_issue_arg.hostname:
2092 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2093
skobes6468b902016-10-24 08:45:10 -07002094 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2095 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2096 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002097 try:
skobes6468b902016-10-24 08:45:10 -07002098 scm_obj.apply_patch(patchset_object)
2099 except Exception as e:
2100 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002101 return 1
2102
2103 # If we had an issue, commit the current state and register the issue.
2104 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002105 self.SetIssue(self.GetIssue())
2106 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2108 'patch from issue %(i)s at patchset '
2109 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2110 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002111 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002113 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 return 0
2115
2116 @staticmethod
2117 def ParseIssueURL(parsed_url):
2118 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2119 return None
wychen3c1c1722016-08-04 11:46:36 -07002120 # Rietveld patch: https://domain/<number>/#ps<patchset>
2121 match = re.match(r'/(\d+)/$', parsed_url.path)
2122 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2123 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002124 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002125 issue=int(match.group(1)),
2126 patchset=int(match2.group(1)),
2127 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002128 # Typical url: https://domain/<issue_number>[/[other]]
2129 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2130 if match:
skobes6468b902016-10-24 08:45:10 -07002131 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002132 issue=int(match.group(1)),
2133 hostname=parsed_url.netloc)
2134 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2135 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2136 if match:
skobes6468b902016-10-24 08:45:10 -07002137 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002138 issue=int(match.group(1)),
2139 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002140 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002141 return None
2142
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 def CMDUploadChange(self, options, args, change):
2144 """Upload the patch to Rietveld."""
2145 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2146 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002147 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2148 if options.emulate_svn_auto_props:
2149 upload_args.append('--emulate_svn_auto_props')
2150
2151 change_desc = None
2152
2153 if options.email is not None:
2154 upload_args.extend(['--email', options.email])
2155
2156 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002157 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 upload_args.extend(['--title', options.title])
2159 if options.message:
2160 upload_args.extend(['--message', options.message])
2161 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print('This branch is associated with issue %s. '
2163 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002164 else:
nodirca166002016-06-27 10:59:51 -07002165 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002167 if options.message:
2168 message = options.message
2169 else:
2170 message = CreateDescriptionFromLog(args)
2171 if options.title:
2172 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002173 change_desc = ChangeDescription(message)
2174 if options.reviewers or options.tbr_owners:
2175 change_desc.update_reviewers(options.reviewers,
2176 options.tbr_owners,
2177 change)
2178 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002179 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002180
2181 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002182 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 return 1
2184
2185 upload_args.extend(['--message', change_desc.description])
2186 if change_desc.get_reviewers():
2187 upload_args.append('--reviewers=%s' % ','.join(
2188 change_desc.get_reviewers()))
2189 if options.send_mail:
2190 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002191 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 upload_args.append('--send_mail')
2193
2194 # We check this before applying rietveld.private assuming that in
2195 # rietveld.cc only addresses which we can send private CLs to are listed
2196 # if rietveld.private is set, and so we should ignore rietveld.cc only
2197 # when --private is specified explicitly on the command line.
2198 if options.private:
2199 logging.warn('rietveld.cc is ignored since private flag is specified. '
2200 'You need to review and add them manually if necessary.')
2201 cc = self.GetCCListWithoutDefault()
2202 else:
2203 cc = self.GetCCList()
2204 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002205 if change_desc.get_cced():
2206 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 if cc:
2208 upload_args.extend(['--cc', cc])
2209
2210 if options.private or settings.GetDefaultPrivateFlag() == "True":
2211 upload_args.append('--private')
2212
2213 upload_args.extend(['--git_similarity', str(options.similarity)])
2214 if not options.find_copies:
2215 upload_args.extend(['--git_no_find_copies'])
2216
2217 # Include the upstream repo's URL in the change -- this is useful for
2218 # projects that have their source spread across multiple repos.
2219 remote_url = self.GetGitBaseUrlFromConfig()
2220 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002221 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2222 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2223 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002225 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002226 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 if target_ref:
2228 upload_args.extend(['--target_ref', target_ref])
2229
2230 # Look for dependent patchsets. See crbug.com/480453 for more details.
2231 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2232 upstream_branch = ShortBranchName(upstream_branch)
2233 if remote is '.':
2234 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002235 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002236 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002237 print()
2238 print('Skipping dependency patchset upload because git config '
2239 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2240 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241 else:
2242 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002243 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 auth_config=auth_config)
2245 branch_cl_issue_url = branch_cl.GetIssueURL()
2246 branch_cl_issue = branch_cl.GetIssue()
2247 branch_cl_patchset = branch_cl.GetPatchset()
2248 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2249 upload_args.extend(
2250 ['--depends_on_patchset', '%s:%s' % (
2251 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002252 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002253 '\n'
2254 'The current branch (%s) is tracking a local branch (%s) with '
2255 'an associated CL.\n'
2256 'Adding %s/#ps%s as a dependency patchset.\n'
2257 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2258 branch_cl_patchset))
2259
2260 project = settings.GetProject()
2261 if project:
2262 upload_args.extend(['--project', project])
2263
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002264 try:
2265 upload_args = ['upload'] + upload_args + args
2266 logging.info('upload.RealMain(%s)', upload_args)
2267 issue, patchset = upload.RealMain(upload_args)
2268 issue = int(issue)
2269 patchset = int(patchset)
2270 except KeyboardInterrupt:
2271 sys.exit(1)
2272 except:
2273 # If we got an exception after the user typed a description for their
2274 # change, back up the description before re-raising.
2275 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002276 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002277 raise
2278
2279 if not self.GetIssue():
2280 self.SetIssue(issue)
2281 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 return 0
2283
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002284
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002285class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002286 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002287 # auth_config is Rietveld thing, kept here to preserve interface only.
2288 super(_GerritChangelistImpl, self).__init__(changelist)
2289 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002290 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002291 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002292 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002293 # Map from change number (issue) to its detail cache.
2294 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002295
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002296 if codereview_host is not None:
2297 assert not codereview_host.startswith('https://'), codereview_host
2298 self._gerrit_host = codereview_host
2299 self._gerrit_server = 'https://%s' % codereview_host
2300
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002301 def _GetGerritHost(self):
2302 # Lazy load of configs.
2303 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002304 if self._gerrit_host and '.' not in self._gerrit_host:
2305 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2306 # This happens for internal stuff http://crbug.com/614312.
2307 parsed = urlparse.urlparse(self.GetRemoteUrl())
2308 if parsed.scheme == 'sso':
2309 print('WARNING: using non https URLs for remote is likely broken\n'
2310 ' Your current remote is: %s' % self.GetRemoteUrl())
2311 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2312 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002313 return self._gerrit_host
2314
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002315 def _GetGitHost(self):
2316 """Returns git host to be used when uploading change to Gerrit."""
2317 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2318
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002319 def GetCodereviewServer(self):
2320 if not self._gerrit_server:
2321 # If we're on a branch then get the server potentially associated
2322 # with that branch.
2323 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002324 self._gerrit_server = self._GitGetBranchConfigValue(
2325 self.CodereviewServerConfigKey())
2326 if self._gerrit_server:
2327 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002328 if not self._gerrit_server:
2329 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2330 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002331 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002332 parts[0] = parts[0] + '-review'
2333 self._gerrit_host = '.'.join(parts)
2334 self._gerrit_server = 'https://%s' % self._gerrit_host
2335 return self._gerrit_server
2336
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002337 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002338 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002339 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002340
tandrii5d48c322016-08-18 16:19:37 -07002341 @classmethod
2342 def PatchsetConfigKey(cls):
2343 return 'gerritpatchset'
2344
2345 @classmethod
2346 def CodereviewServerConfigKey(cls):
2347 return 'gerritserver'
2348
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002349 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002350 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002351 if settings.GetGerritSkipEnsureAuthenticated():
2352 # For projects with unusual authentication schemes.
2353 # See http://crbug.com/603378.
2354 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002355 # Lazy-loader to identify Gerrit and Git hosts.
2356 if gerrit_util.GceAuthenticator.is_gce():
2357 return
2358 self.GetCodereviewServer()
2359 git_host = self._GetGitHost()
2360 assert self._gerrit_server and self._gerrit_host
2361 cookie_auth = gerrit_util.CookiesAuthenticator()
2362
2363 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2364 git_auth = cookie_auth.get_auth_header(git_host)
2365 if gerrit_auth and git_auth:
2366 if gerrit_auth == git_auth:
2367 return
2368 print((
2369 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2370 ' Check your %s or %s file for credentials of hosts:\n'
2371 ' %s\n'
2372 ' %s\n'
2373 ' %s') %
2374 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2375 git_host, self._gerrit_host,
2376 cookie_auth.get_new_password_message(git_host)))
2377 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002378 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002379 return
2380 else:
2381 missing = (
2382 [] if gerrit_auth else [self._gerrit_host] +
2383 [] if git_auth else [git_host])
2384 DieWithError('Credentials for the following hosts are required:\n'
2385 ' %s\n'
2386 'These are read from %s (or legacy %s)\n'
2387 '%s' % (
2388 '\n '.join(missing),
2389 cookie_auth.get_gitcookies_path(),
2390 cookie_auth.get_netrc_path(),
2391 cookie_auth.get_new_password_message(git_host)))
2392
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002393 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002394 if not self.GetIssue():
2395 return
2396
2397 # Warm change details cache now to avoid RPCs later, reducing latency for
2398 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002399 self._GetChangeDetail(
2400 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002401
2402 status = self._GetChangeDetail()['status']
2403 if status in ('MERGED', 'ABANDONED'):
2404 DieWithError('Change %s has been %s, new uploads are not allowed' %
2405 (self.GetIssueURL(),
2406 'submitted' if status == 'MERGED' else 'abandoned'))
2407
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002408 if gerrit_util.GceAuthenticator.is_gce():
2409 return
2410 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2411 self._GetGerritHost())
2412 if self.GetIssueOwner() == cookies_user:
2413 return
2414 logging.debug('change %s owner is %s, cookies user is %s',
2415 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2416 # Maybe user has linked accounts or smth like that,
2417 # so ask what Gerrit thinks of this user.
2418 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2419 if details['email'] == self.GetIssueOwner():
2420 return
2421 if not force:
2422 print('WARNING: change %s is owned by %s, but you authenticate to Gerrit '
2423 'as %s.\n'
2424 'Uploading may fail due to lack of permissions.' %
2425 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2426 confirm_or_exit(action='upload')
2427
2428
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002429 def _PostUnsetIssueProperties(self):
2430 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002431 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002432
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002433 def GetRieveldObjForPresubmit(self):
2434 class ThisIsNotRietveldIssue(object):
2435 def __nonzero__(self):
2436 # This is a hack to make presubmit_support think that rietveld is not
2437 # defined, yet still ensure that calls directly result in a decent
2438 # exception message below.
2439 return False
2440
2441 def __getattr__(self, attr):
2442 print(
2443 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2444 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2445 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2446 'or use Rietveld for codereview.\n'
2447 'See also http://crbug.com/579160.' % attr)
2448 raise NotImplementedError()
2449 return ThisIsNotRietveldIssue()
2450
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002451 def GetGerritObjForPresubmit(self):
2452 return presubmit_support.GerritAccessor(self._GetGerritHost())
2453
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002454 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002455 """Apply a rough heuristic to give a simple summary of an issue's review
2456 or CQ status, assuming adherence to a common workflow.
2457
2458 Returns None if no issue for this branch, or one of the following keywords:
2459 * 'error' - error from review tool (including deleted issues)
2460 * 'unsent' - no reviewers added
2461 * 'waiting' - waiting for review
2462 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002463 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002464 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002465 * 'commit' - in the commit queue
2466 * 'closed' - abandoned
2467 """
2468 if not self.GetIssue():
2469 return None
2470
2471 try:
2472 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002473 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002474 return 'error'
2475
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002476 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002477 return 'closed'
2478
2479 cq_label = data['labels'].get('Commit-Queue', {})
2480 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002481 votes = cq_label.get('all', [])
2482 highest_vote = 0
2483 for v in votes:
2484 highest_vote = max(highest_vote, v.get('value', 0))
2485 vote_value = str(highest_vote)
2486 if vote_value != '0':
2487 # Add a '+' if the value is not 0 to match the values in the label.
2488 # The cq_label does not have negatives.
2489 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002490 vote_text = cq_label.get('values', {}).get(vote_value, '')
2491 if vote_text.lower() == 'commit':
2492 return 'commit'
2493
2494 lgtm_label = data['labels'].get('Code-Review', {})
2495 if lgtm_label:
2496 if 'rejected' in lgtm_label:
2497 return 'not lgtm'
2498 if 'approved' in lgtm_label:
2499 return 'lgtm'
2500
2501 if not data.get('reviewers', {}).get('REVIEWER', []):
2502 return 'unsent'
2503
2504 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002505 owner = data['owner'].get('_account_id')
2506 while messages:
2507 last_message_author = messages.pop().get('author', {})
2508 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2509 # Ignore replies from CQ.
2510 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002511 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002512 # Some reply from non-owner.
2513 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002514 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002515
2516 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002517 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002518 return data['revisions'][data['current_revision']]['_number']
2519
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002520 def FetchDescription(self, force=False):
2521 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2522 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002523 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002524 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002525
dsansomee2d6fd92016-09-08 00:10:47 -07002526 def UpdateDescriptionRemote(self, description, force=False):
2527 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2528 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002529 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002530 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002531 'unpublished edit. Either publish the edit in the Gerrit web UI '
2532 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002533
2534 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2535 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002536 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002537 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002538
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002539 def AddComment(self, message):
2540 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2541 msg=message)
2542
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002543 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002544 # DETAILED_ACCOUNTS is to get emails in accounts.
2545 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2546 summary = []
2547 for msg in data.get('messages', []):
2548 # Gerrit spits out nanoseconds.
2549 assert len(msg['date'].split('.')[-1]) == 9
2550 date = datetime.datetime.strptime(msg['date'][:-3],
2551 '%Y-%m-%d %H:%M:%S.%f')
2552 summary.append(_CommentSummary(
2553 date=date,
2554 message=msg['message'],
2555 sender=msg['author']['email'],
2556 # These could be inferred from the text messages and correlated with
2557 # Code-Review label maximum, however this is not reliable.
2558 # Leaving as is until the need arises.
2559 approval=False,
2560 disapproval=False,
2561 ))
2562 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002563
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002564 def CloseIssue(self):
2565 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2566
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002567 def GetApprovingReviewers(self):
2568 """Returns a list of reviewers approving the change.
2569
2570 Note: not necessarily committers.
2571 """
2572 raise NotImplementedError()
2573
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002574 def SubmitIssue(self, wait_for_merge=True):
2575 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2576 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002577
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002578 def _GetChangeDetail(self, options=None, issue=None,
2579 no_cache=False):
2580 """Returns details of the issue by querying Gerrit and caching results.
2581
2582 If fresh data is needed, set no_cache=True which will clear cache and
2583 thus new data will be fetched from Gerrit.
2584 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002585 options = options or []
2586 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002587 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002588
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002589 # Optimization to avoid multiple RPCs:
2590 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2591 'CURRENT_COMMIT' not in options):
2592 options.append('CURRENT_COMMIT')
2593
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002594 # Normalize issue and options for consistent keys in cache.
2595 issue = str(issue)
2596 options = [o.upper() for o in options]
2597
2598 # Check in cache first unless no_cache is True.
2599 if no_cache:
2600 self._detail_cache.pop(issue, None)
2601 else:
2602 options_set = frozenset(options)
2603 for cached_options_set, data in self._detail_cache.get(issue, []):
2604 # Assumption: data fetched before with extra options is suitable
2605 # for return for a smaller set of options.
2606 # For example, if we cached data for
2607 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2608 # and request is for options=[CURRENT_REVISION],
2609 # THEN we can return prior cached data.
2610 if options_set.issubset(cached_options_set):
2611 return data
2612
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002613 try:
2614 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2615 options, ignore_404=False)
2616 except gerrit_util.GerritError as e:
2617 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002618 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002619 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002620
2621 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002622 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002623
agable32978d92016-11-01 12:55:02 -07002624 def _GetChangeCommit(self, issue=None):
2625 issue = issue or self.GetIssue()
2626 assert issue, 'issue is required to query Gerrit'
2627 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2628 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002629 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002630 return data
2631
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002632 def CMDLand(self, force, bypass_hooks, verbose):
2633 if git_common.is_dirty_git_tree('land'):
2634 return 1
tandriid60367b2016-06-22 05:25:12 -07002635 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2636 if u'Commit-Queue' in detail.get('labels', {}):
2637 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002638 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2639 'which can test and land changes for you. '
2640 'Are you sure you wish to bypass it?\n',
2641 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002642
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002643 differs = True
tandriic4344b52016-08-29 06:04:54 -07002644 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002645 # Note: git diff outputs nothing if there is no diff.
2646 if not last_upload or RunGit(['diff', last_upload]).strip():
2647 print('WARNING: some changes from local branch haven\'t been uploaded')
2648 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002649 if detail['current_revision'] == last_upload:
2650 differs = False
2651 else:
2652 print('WARNING: local branch contents differ from latest uploaded '
2653 'patchset')
2654 if differs:
2655 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002656 confirm_or_exit(
2657 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2658 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002659 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2660 elif not bypass_hooks:
2661 hook_results = self.RunHook(
2662 committing=True,
2663 may_prompt=not force,
2664 verbose=verbose,
2665 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2666 if not hook_results.should_continue():
2667 return 1
2668
2669 self.SubmitIssue(wait_for_merge=True)
2670 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002671 links = self._GetChangeCommit().get('web_links', [])
2672 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002673 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002674 print('Landed as %s' % link.get('url'))
2675 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002676 return 0
2677
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002678 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2679 directory):
2680 assert not reject
2681 assert not nocommit
2682 assert not directory
2683 assert parsed_issue_arg.valid
2684
2685 self._changelist.issue = parsed_issue_arg.issue
2686
2687 if parsed_issue_arg.hostname:
2688 self._gerrit_host = parsed_issue_arg.hostname
2689 self._gerrit_server = 'https://%s' % self._gerrit_host
2690
tandriic2405f52016-10-10 08:13:15 -07002691 try:
2692 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002693 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002694 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002695
2696 if not parsed_issue_arg.patchset:
2697 # Use current revision by default.
2698 revision_info = detail['revisions'][detail['current_revision']]
2699 patchset = int(revision_info['_number'])
2700 else:
2701 patchset = parsed_issue_arg.patchset
2702 for revision_info in detail['revisions'].itervalues():
2703 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2704 break
2705 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002706 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002707 (parsed_issue_arg.patchset, self.GetIssue()))
2708
2709 fetch_info = revision_info['fetch']['http']
2710 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002711 self.SetIssue(self.GetIssue())
2712 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002713 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002714 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002715 (self.GetIssue(), self.GetPatchset()))
2716 return 0
2717
2718 @staticmethod
2719 def ParseIssueURL(parsed_url):
2720 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2721 return None
2722 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2723 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2724 # Short urls like https://domain/<issue_number> can be used, but don't allow
2725 # specifying the patchset (you'd 404), but we allow that here.
2726 if parsed_url.path == '/':
2727 part = parsed_url.fragment
2728 else:
2729 part = parsed_url.path
2730 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2731 if match:
2732 return _ParsedIssueNumberArgument(
2733 issue=int(match.group(2)),
2734 patchset=int(match.group(4)) if match.group(4) else None,
2735 hostname=parsed_url.netloc)
2736 return None
2737
tandrii16e0b4e2016-06-07 10:34:28 -07002738 def _GerritCommitMsgHookCheck(self, offer_removal):
2739 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2740 if not os.path.exists(hook):
2741 return
2742 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2743 # custom developer made one.
2744 data = gclient_utils.FileRead(hook)
2745 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2746 return
2747 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002748 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002749 'and may interfere with it in subtle ways.\n'
2750 'We recommend you remove the commit-msg hook.')
2751 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002752 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002753 gclient_utils.rm_file_or_tree(hook)
2754 print('Gerrit commit-msg hook removed.')
2755 else:
2756 print('OK, will keep Gerrit commit-msg hook in place.')
2757
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002758 def CMDUploadChange(self, options, args, change):
2759 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002760 if options.squash and options.no_squash:
2761 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002762
2763 if not options.squash and not options.no_squash:
2764 # Load default for user, repo, squash=true, in this order.
2765 options.squash = settings.GetSquashGerritUploads()
2766 elif options.no_squash:
2767 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002768
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769 # We assume the remote called "origin" is the one we want.
2770 # It is probably not worthwhile to support different workflows.
2771 gerrit_remote = 'origin'
2772
2773 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002774 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002775
Aaron Gableb56ad332017-01-06 15:24:31 -08002776 # This may be None; default fallback value is determined in logic below.
2777 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002778 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002779
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002781 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002782 if self.GetIssue():
2783 # Try to get the message from a previous upload.
2784 message = self.GetDescription()
2785 if not message:
2786 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002787 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002788 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002789 if not title:
2790 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2791 title = ask_for_data(
2792 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002793 if title == default_title:
2794 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002795 change_id = self._GetChangeDetail()['change_id']
2796 while True:
2797 footer_change_ids = git_footers.get_footer_change_id(message)
2798 if footer_change_ids == [change_id]:
2799 break
2800 if not footer_change_ids:
2801 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002802 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803 continue
2804 # There is already a valid footer but with different or several ids.
2805 # Doing this automatically is non-trivial as we don't want to lose
2806 # existing other footers, yet we want to append just 1 desired
2807 # Change-Id. Thus, just create a new footer, but let user verify the
2808 # new description.
2809 message = '%s\n\nChange-Id: %s' % (message, change_id)
2810 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002811 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002812 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002813 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002814 'Please, check the proposed correction to the description, '
2815 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2816 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2817 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002818 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002819 if not options.force:
2820 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002821 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002822 message = change_desc.description
2823 if not message:
2824 DieWithError("Description is empty. Aborting...")
2825 # Continue the while loop.
2826 # Sanity check of this code - we should end up with proper message
2827 # footer.
2828 assert [change_id] == git_footers.get_footer_change_id(message)
2829 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002830 else: # if not self.GetIssue()
2831 if options.message:
2832 message = options.message
2833 else:
2834 message = CreateDescriptionFromLog(args)
2835 if options.title:
2836 message = options.title + '\n\n' + message
2837 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002838 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002839 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002840 # On first upload, patchset title is always this string, while
2841 # --title flag gets converted to first line of message.
2842 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002843 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 if not change_desc.description:
2845 DieWithError("Description is empty. Aborting...")
2846 message = change_desc.description
2847 change_ids = git_footers.get_footer_change_id(message)
2848 if len(change_ids) > 1:
2849 DieWithError('too many Change-Id footers, at most 1 allowed.')
2850 if not change_ids:
2851 # Generate the Change-Id automatically.
2852 message = git_footers.add_footer_change_id(
2853 message, GenerateGerritChangeId(message))
2854 change_desc.set_description(message)
2855 change_ids = git_footers.get_footer_change_id(message)
2856 assert len(change_ids) == 1
2857 change_id = change_ids[0]
2858
2859 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2860 if remote is '.':
2861 # If our upstream branch is local, we base our squashed commit on its
2862 # squashed version.
2863 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2864 # Check the squashed hash of the parent.
2865 parent = RunGit(['config',
2866 'branch.%s.gerritsquashhash' % upstream_branch_name],
2867 error_ok=True).strip()
2868 # Verify that the upstream branch has been uploaded too, otherwise
2869 # Gerrit will create additional CLs when uploading.
2870 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2871 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002872 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002873 '\nUpload upstream branch %s first.\n'
2874 'It is likely that this branch has been rebased since its last '
2875 'upload, so you just need to upload it again.\n'
2876 '(If you uploaded it with --no-squash, then branch dependencies '
2877 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002878 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002879 else:
2880 parent = self.GetCommonAncestorWithUpstream()
2881
2882 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2883 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2884 '-m', message]).strip()
2885 else:
2886 change_desc = ChangeDescription(
2887 options.message or CreateDescriptionFromLog(args))
2888 if not change_desc.description:
2889 DieWithError("Description is empty. Aborting...")
2890
2891 if not git_footers.get_footer_change_id(change_desc.description):
2892 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002893 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2894 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002895 ref_to_push = 'HEAD'
2896 parent = '%s/%s' % (gerrit_remote, branch)
2897 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2898
2899 assert change_desc
2900 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2901 ref_to_push)]).splitlines()
2902 if len(commits) > 1:
2903 print('WARNING: This will upload %d commits. Run the following command '
2904 'to see which commits will be uploaded: ' % len(commits))
2905 print('git log %s..%s' % (parent, ref_to_push))
2906 print('You can also use `git squash-branch` to squash these into a '
2907 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002908 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002909
2910 if options.reviewers or options.tbr_owners:
2911 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2912 change)
2913
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002914 # Extra options that can be specified at push time. Doc:
2915 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2916 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002917 if change_desc.get_reviewers(tbr_only=True):
2918 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2919 refspec_opts.append('l=Code-Review+1')
2920
Aaron Gable9b713dd2016-12-14 16:04:21 -08002921 if title:
2922 if not re.match(r'^[\w ]+$', title):
2923 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002924 if not automatic_title:
2925 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002926 'and spaces. You can edit it in the UI. '
2927 'See https://crbug.com/663787.\n'
2928 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002929 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2930 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002931 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002932
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002933 if options.send_mail:
2934 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002935 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002936 refspec_opts.append('notify=ALL')
2937 else:
2938 refspec_opts.append('notify=NONE')
2939
tandrii99a72f22016-08-17 14:33:24 -07002940 reviewers = change_desc.get_reviewers()
2941 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002942 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2943 # side for real (b/34702620).
2944 def clean_invisible_chars(email):
2945 return email.decode('unicode_escape').encode('ascii', 'ignore')
2946 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2947 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002948
agablec6787972016-09-09 16:13:34 -07002949 if options.private:
2950 refspec_opts.append('draft')
2951
rmistry9eadede2016-09-19 11:22:43 -07002952 if options.topic:
2953 # Documentation on Gerrit topics is here:
2954 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2955 refspec_opts.append('topic=%s' % options.topic)
2956
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002957 refspec_suffix = ''
2958 if refspec_opts:
2959 refspec_suffix = '%' + ','.join(refspec_opts)
2960 assert ' ' not in refspec_suffix, (
2961 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002962 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002963
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002964 try:
2965 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002966 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002967 print_stdout=True,
2968 # Flush after every line: useful for seeing progress when running as
2969 # recipe.
2970 filter_fn=lambda _: sys.stdout.flush())
2971 except subprocess2.CalledProcessError:
2972 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002973 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002974
2975 if options.squash:
2976 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2977 change_numbers = [m.group(1)
2978 for m in map(regex.match, push_stdout.splitlines())
2979 if m]
2980 if len(change_numbers) != 1:
2981 DieWithError(
2982 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002983 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002984 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002985 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002986
2987 # Add cc's from the CC_LIST and --cc flag (if any).
2988 cc = self.GetCCList().split(',')
2989 if options.cc:
2990 cc.extend(options.cc)
2991 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002992 if change_desc.get_cced():
2993 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002994 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002995 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002996 self._GetGerritHost(), self.GetIssue(), cc,
2997 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002998 return 0
2999
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003000 def _AddChangeIdToCommitMessage(self, options, args):
3001 """Re-commits using the current message, assumes the commit hook is in
3002 place.
3003 """
3004 log_desc = options.message or CreateDescriptionFromLog(args)
3005 git_command = ['commit', '--amend', '-m', log_desc]
3006 RunGit(git_command)
3007 new_log_desc = CreateDescriptionFromLog(args)
3008 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003009 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003010 return new_log_desc
3011 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003012 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003013
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003014 def SetCQState(self, new_state):
3015 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003016 vote_map = {
3017 _CQState.NONE: 0,
3018 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003019 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003020 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01003021 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
3022 if new_state == _CQState.DRY_RUN:
3023 # Don't spam everybody reviewer/owner.
3024 kwargs['notify'] = 'NONE'
3025 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003026
tandriie113dfd2016-10-11 10:20:12 -07003027 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003028 try:
3029 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003030 except GerritChangeNotExists:
3031 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003032
3033 if data['status'] in ('ABANDONED', 'MERGED'):
3034 return 'CL %s is closed' % self.GetIssue()
3035
3036 def GetTryjobProperties(self, patchset=None):
3037 """Returns dictionary of properties to launch tryjob."""
3038 data = self._GetChangeDetail(['ALL_REVISIONS'])
3039 patchset = int(patchset or self.GetPatchset())
3040 assert patchset
3041 revision_data = None # Pylint wants it to be defined.
3042 for revision_data in data['revisions'].itervalues():
3043 if int(revision_data['_number']) == patchset:
3044 break
3045 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003046 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003047 (patchset, self.GetIssue()))
3048 return {
3049 'patch_issue': self.GetIssue(),
3050 'patch_set': patchset or self.GetPatchset(),
3051 'patch_project': data['project'],
3052 'patch_storage': 'gerrit',
3053 'patch_ref': revision_data['fetch']['http']['ref'],
3054 'patch_repository_url': revision_data['fetch']['http']['url'],
3055 'patch_gerrit_url': self.GetCodereviewServer(),
3056 }
tandriie113dfd2016-10-11 10:20:12 -07003057
tandriide281ae2016-10-12 06:02:30 -07003058 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003059 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003060
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003061
3062_CODEREVIEW_IMPLEMENTATIONS = {
3063 'rietveld': _RietveldChangelistImpl,
3064 'gerrit': _GerritChangelistImpl,
3065}
3066
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003067
iannuccie53c9352016-08-17 14:40:40 -07003068def _add_codereview_issue_select_options(parser, extra=""):
3069 _add_codereview_select_options(parser)
3070
3071 text = ('Operate on this issue number instead of the current branch\'s '
3072 'implicit issue.')
3073 if extra:
3074 text += ' '+extra
3075 parser.add_option('-i', '--issue', type=int, help=text)
3076
3077
3078def _process_codereview_issue_select_options(parser, options):
3079 _process_codereview_select_options(parser, options)
3080 if options.issue is not None and not options.forced_codereview:
3081 parser.error('--issue must be specified with either --rietveld or --gerrit')
3082
3083
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003084def _add_codereview_select_options(parser):
3085 """Appends --gerrit and --rietveld options to force specific codereview."""
3086 parser.codereview_group = optparse.OptionGroup(
3087 parser, 'EXPERIMENTAL! Codereview override options')
3088 parser.add_option_group(parser.codereview_group)
3089 parser.codereview_group.add_option(
3090 '--gerrit', action='store_true',
3091 help='Force the use of Gerrit for codereview')
3092 parser.codereview_group.add_option(
3093 '--rietveld', action='store_true',
3094 help='Force the use of Rietveld for codereview')
3095
3096
3097def _process_codereview_select_options(parser, options):
3098 if options.gerrit and options.rietveld:
3099 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3100 options.forced_codereview = None
3101 if options.gerrit:
3102 options.forced_codereview = 'gerrit'
3103 elif options.rietveld:
3104 options.forced_codereview = 'rietveld'
3105
3106
tandriif9aefb72016-07-01 09:06:51 -07003107def _get_bug_line_values(default_project, bugs):
3108 """Given default_project and comma separated list of bugs, yields bug line
3109 values.
3110
3111 Each bug can be either:
3112 * a number, which is combined with default_project
3113 * string, which is left as is.
3114
3115 This function may produce more than one line, because bugdroid expects one
3116 project per line.
3117
3118 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3119 ['v8:123', 'chromium:789']
3120 """
3121 default_bugs = []
3122 others = []
3123 for bug in bugs.split(','):
3124 bug = bug.strip()
3125 if bug:
3126 try:
3127 default_bugs.append(int(bug))
3128 except ValueError:
3129 others.append(bug)
3130
3131 if default_bugs:
3132 default_bugs = ','.join(map(str, default_bugs))
3133 if default_project:
3134 yield '%s:%s' % (default_project, default_bugs)
3135 else:
3136 yield default_bugs
3137 for other in sorted(others):
3138 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3139 yield other
3140
3141
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003142class ChangeDescription(object):
3143 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003144 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003145 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003146 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003147 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003148
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003149 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003150 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151
agable@chromium.org42c20792013-09-12 17:34:49 +00003152 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003153 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 return '\n'.join(self._description_lines)
3155
3156 def set_description(self, desc):
3157 if isinstance(desc, basestring):
3158 lines = desc.splitlines()
3159 else:
3160 lines = [line.rstrip() for line in desc]
3161 while lines and not lines[0]:
3162 lines.pop(0)
3163 while lines and not lines[-1]:
3164 lines.pop(-1)
3165 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003166
piman@chromium.org336f9122014-09-04 02:16:55 +00003167 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003168 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003169 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003170 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003171 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003172 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003173
agable@chromium.org42c20792013-09-12 17:34:49 +00003174 # Get the set of R= and TBR= lines and remove them from the desciption.
3175 regexp = re.compile(self.R_LINE)
3176 matches = [regexp.match(line) for line in self._description_lines]
3177 new_desc = [l for i, l in enumerate(self._description_lines)
3178 if not matches[i]]
3179 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003180
agable@chromium.org42c20792013-09-12 17:34:49 +00003181 # Construct new unified R= and TBR= lines.
3182 r_names = []
3183 tbr_names = []
3184 for match in matches:
3185 if not match:
3186 continue
3187 people = cleanup_list([match.group(2).strip()])
3188 if match.group(1) == 'TBR':
3189 tbr_names.extend(people)
3190 else:
3191 r_names.extend(people)
3192 for name in r_names:
3193 if name not in reviewers:
3194 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003195 if add_owners_tbr:
3196 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003197 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003198 all_reviewers = set(tbr_names + reviewers)
3199 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3200 all_reviewers)
3201 tbr_names.extend(owners_db.reviewers_for(missing_files,
3202 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003203 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3204 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3205
3206 # Put the new lines in the description where the old first R= line was.
3207 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3208 if 0 <= line_loc < len(self._description_lines):
3209 if new_tbr_line:
3210 self._description_lines.insert(line_loc, new_tbr_line)
3211 if new_r_line:
3212 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003213 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003214 if new_r_line:
3215 self.append_footer(new_r_line)
3216 if new_tbr_line:
3217 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003218
Aaron Gable3a16ed12017-03-23 10:51:55 -07003219 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003220 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003221 self.set_description([
3222 '# Enter a description of the change.',
3223 '# This will be displayed on the codereview site.',
3224 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003225 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003226 '--------------------',
3227 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003228
agable@chromium.org42c20792013-09-12 17:34:49 +00003229 regexp = re.compile(self.BUG_LINE)
3230 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003231 prefix = settings.GetBugPrefix()
3232 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003233 if git_footer:
3234 self.append_footer('Bug: %s' % ', '.join(values))
3235 else:
3236 for value in values:
3237 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003238
agable@chromium.org42c20792013-09-12 17:34:49 +00003239 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003240 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003241 if not content:
3242 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003243 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003244
3245 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003246 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3247 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003248 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003249 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003250
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003251 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003252 """Adds a footer line to the description.
3253
3254 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3255 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3256 that Gerrit footers are always at the end.
3257 """
3258 parsed_footer_line = git_footers.parse_footer(line)
3259 if parsed_footer_line:
3260 # Line is a gerrit footer in the form: Footer-Key: any value.
3261 # Thus, must be appended observing Gerrit footer rules.
3262 self.set_description(
3263 git_footers.add_footer(self.description,
3264 key=parsed_footer_line[0],
3265 value=parsed_footer_line[1]))
3266 return
3267
3268 if not self._description_lines:
3269 self._description_lines.append(line)
3270 return
3271
3272 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3273 if gerrit_footers:
3274 # git_footers.split_footers ensures that there is an empty line before
3275 # actual (gerrit) footers, if any. We have to keep it that way.
3276 assert top_lines and top_lines[-1] == ''
3277 top_lines, separator = top_lines[:-1], top_lines[-1:]
3278 else:
3279 separator = [] # No need for separator if there are no gerrit_footers.
3280
3281 prev_line = top_lines[-1] if top_lines else ''
3282 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3283 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3284 top_lines.append('')
3285 top_lines.append(line)
3286 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003287
tandrii99a72f22016-08-17 14:33:24 -07003288 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003289 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003290 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003291 reviewers = [match.group(2).strip()
3292 for match in matches
3293 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003294 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003295
bradnelsond975b302016-10-23 12:20:23 -07003296 def get_cced(self):
3297 """Retrieves the list of reviewers."""
3298 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3299 cced = [match.group(2).strip() for match in matches if match]
3300 return cleanup_list(cced)
3301
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003302 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3303 """Updates this commit description given the parent.
3304
3305 This is essentially what Gnumbd used to do.
3306 Consult https://goo.gl/WMmpDe for more details.
3307 """
3308 assert parent_msg # No, orphan branch creation isn't supported.
3309 assert parent_hash
3310 assert dest_ref
3311 parent_footer_map = git_footers.parse_footers(parent_msg)
3312 # This will also happily parse svn-position, which GnumbD is no longer
3313 # supporting. While we'd generate correct footers, the verifier plugin
3314 # installed in Gerrit will block such commit (ie git push below will fail).
3315 parent_position = git_footers.get_position(parent_footer_map)
3316
3317 # Cherry-picks may have last line obscuring their prior footers,
3318 # from git_footers perspective. This is also what Gnumbd did.
3319 cp_line = None
3320 if (self._description_lines and
3321 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3322 cp_line = self._description_lines.pop()
3323
3324 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3325
3326 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3327 # user interference with actual footers we'd insert below.
3328 for i, (k, v) in enumerate(parsed_footers):
3329 if k.startswith('Cr-'):
3330 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3331
3332 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003333 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003334 if parent_position[0] == dest_ref:
3335 # Same branch as parent.
3336 number = int(parent_position[1]) + 1
3337 else:
3338 number = 1 # New branch, and extra lineage.
3339 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3340 int(parent_position[1])))
3341
3342 parsed_footers.append(('Cr-Commit-Position',
3343 '%s@{#%d}' % (dest_ref, number)))
3344 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3345
3346 self._description_lines = top_lines
3347 if cp_line:
3348 self._description_lines.append(cp_line)
3349 if self._description_lines[-1] != '':
3350 self._description_lines.append('') # Ensure footer separator.
3351 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3352
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003353
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003354def get_approving_reviewers(props):
3355 """Retrieves the reviewers that approved a CL from the issue properties with
3356 messages.
3357
3358 Note that the list may contain reviewers that are not committer, thus are not
3359 considered by the CQ.
3360 """
3361 return sorted(
3362 set(
3363 message['sender']
3364 for message in props['messages']
3365 if message['approval'] and message['sender'] in props['reviewers']
3366 )
3367 )
3368
3369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370def FindCodereviewSettingsFile(filename='codereview.settings'):
3371 """Finds the given file starting in the cwd and going up.
3372
3373 Only looks up to the top of the repository unless an
3374 'inherit-review-settings-ok' file exists in the root of the repository.
3375 """
3376 inherit_ok_file = 'inherit-review-settings-ok'
3377 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003378 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3380 root = '/'
3381 while True:
3382 if filename in os.listdir(cwd):
3383 if os.path.isfile(os.path.join(cwd, filename)):
3384 return open(os.path.join(cwd, filename))
3385 if cwd == root:
3386 break
3387 cwd = os.path.dirname(cwd)
3388
3389
3390def LoadCodereviewSettingsFromFile(fileobj):
3391 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003392 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003394 def SetProperty(name, setting, unset_error_ok=False):
3395 fullname = 'rietveld.' + name
3396 if setting in keyvals:
3397 RunGit(['config', fullname, keyvals[setting]])
3398 else:
3399 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3400
tandrii48df5812016-10-17 03:55:37 -07003401 if not keyvals.get('GERRIT_HOST', False):
3402 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003403 # Only server setting is required. Other settings can be absent.
3404 # In that case, we ignore errors raised during option deletion attempt.
3405 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003406 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003407 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3408 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003409 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003410 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3411 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003412 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003413 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3414 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003416 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003417 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003418
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003419 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003420 RunGit(['config', 'gerrit.squash-uploads',
3421 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003422
tandrii@chromium.org28253532016-04-14 13:46:56 +00003423 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003424 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003425 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3426
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003427 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003428 # should be of the form
3429 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3430 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003431 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3432 keyvals['ORIGIN_URL_CONFIG']])
3433
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003434
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003435def urlretrieve(source, destination):
3436 """urllib is broken for SSL connections via a proxy therefore we
3437 can't use urllib.urlretrieve()."""
3438 with open(destination, 'w') as f:
3439 f.write(urllib2.urlopen(source).read())
3440
3441
ukai@chromium.org712d6102013-11-27 00:52:58 +00003442def hasSheBang(fname):
3443 """Checks fname is a #! script."""
3444 with open(fname) as f:
3445 return f.read(2).startswith('#!')
3446
3447
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003448# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3449def DownloadHooks(*args, **kwargs):
3450 pass
3451
3452
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003453def DownloadGerritHook(force):
3454 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003455
3456 Args:
3457 force: True to update hooks. False to install hooks if not present.
3458 """
3459 if not settings.GetIsGerrit():
3460 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003461 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003462 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3463 if not os.access(dst, os.X_OK):
3464 if os.path.exists(dst):
3465 if not force:
3466 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003467 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003468 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003469 if not hasSheBang(dst):
3470 DieWithError('Not a script: %s\n'
3471 'You need to download from\n%s\n'
3472 'into .git/hooks/commit-msg and '
3473 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003474 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3475 except Exception:
3476 if os.path.exists(dst):
3477 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003478 DieWithError('\nFailed to download hooks.\n'
3479 'You need to download from\n%s\n'
3480 'into .git/hooks/commit-msg and '
3481 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003482
3483
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003484def GetRietveldCodereviewSettingsInteractively():
3485 """Prompt the user for settings."""
3486 server = settings.GetDefaultServerUrl(error_ok=True)
3487 prompt = 'Rietveld server (host[:port])'
3488 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3489 newserver = ask_for_data(prompt + ':')
3490 if not server and not newserver:
3491 newserver = DEFAULT_SERVER
3492 if newserver:
3493 newserver = gclient_utils.UpgradeToHttps(newserver)
3494 if newserver != server:
3495 RunGit(['config', 'rietveld.server', newserver])
3496
3497 def SetProperty(initial, caption, name, is_url):
3498 prompt = caption
3499 if initial:
3500 prompt += ' ("x" to clear) [%s]' % initial
3501 new_val = ask_for_data(prompt + ':')
3502 if new_val == 'x':
3503 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3504 elif new_val:
3505 if is_url:
3506 new_val = gclient_utils.UpgradeToHttps(new_val)
3507 if new_val != initial:
3508 RunGit(['config', 'rietveld.' + name, new_val])
3509
3510 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3511 SetProperty(settings.GetDefaultPrivateFlag(),
3512 'Private flag (rietveld only)', 'private', False)
3513 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3514 'tree-status-url', False)
3515 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3516 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3517 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3518 'run-post-upload-hook', False)
3519
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003520
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003521class _GitCookiesChecker(object):
3522 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003523
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003524 _GOOGLESOURCE = 'googlesource.com'
3525
3526 def __init__(self):
3527 # Cached list of [host, identity, source], where source is either
3528 # .gitcookies or .netrc.
3529 self._all_hosts = None
3530
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003531 def ensure_configured_gitcookies(self):
3532 """Runs checks and suggests fixes to make git use .gitcookies from default
3533 path."""
3534 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3535 configured_path = RunGitSilent(
3536 ['config', '--global', 'http.cookiefile']).strip()
3537 if configured_path:
3538 self._ensure_default_gitcookies_path(configured_path, default)
3539 else:
3540 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003541
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003542 @staticmethod
3543 def _ensure_default_gitcookies_path(configured_path, default_path):
3544 assert configured_path
3545 if configured_path == default_path:
3546 print('git is already configured to use your .gitcookies from %s' %
3547 configured_path)
3548 return
3549
3550 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3551 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3552 (configured_path, default_path))
3553
3554 if not os.path.exists(configured_path):
3555 print('However, your configured .gitcookies file is missing.')
3556 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3557 action='reconfigure')
3558 RunGit(['config', '--global', 'http.cookiefile', default_path])
3559 return
3560
3561 if os.path.exists(default_path):
3562 print('WARNING: default .gitcookies file already exists %s' %
3563 default_path)
3564 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3565 default_path)
3566
3567 confirm_or_exit('Move existing .gitcookies to default location?',
3568 action='move')
3569 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003570 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003571 print('Moved and reconfigured git to use .gitcookies from %s' %
3572 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003573
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003574 @staticmethod
3575 def _configure_gitcookies_path(default_path):
3576 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3577 if os.path.exists(netrc_path):
3578 print('You seem to be using outdated .netrc for git credentials: %s' %
3579 netrc_path)
3580 print('This tool will guide you through setting up recommended '
3581 '.gitcookies store for git credentials.\n'
3582 '\n'
3583 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3584 ' git config --global --unset http.cookiefile\n'
3585 ' mv %s %s.backup\n\n' % (default_path, default_path))
3586 confirm_or_exit(action='setup .gitcookies')
3587 RunGit(['config', '--global', 'http.cookiefile', default_path])
3588 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003589
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003590 def get_hosts_with_creds(self, include_netrc=False):
3591 if self._all_hosts is None:
3592 a = gerrit_util.CookiesAuthenticator()
3593 self._all_hosts = [
3594 (h, u, s)
3595 for h, u, s in itertools.chain(
3596 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3597 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3598 )
3599 if h.endswith(self._GOOGLESOURCE)
3600 ]
3601
3602 if include_netrc:
3603 return self._all_hosts
3604 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3605
3606 def print_current_creds(self, include_netrc=False):
3607 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3608 if not hosts:
3609 print('No Git/Gerrit credentials found')
3610 return
3611 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3612 header = [('Host', 'User', 'Which file'),
3613 ['=' * l for l in lengths]]
3614 for row in (header + hosts):
3615 print('\t'.join((('%%+%ds' % l) % s)
3616 for l, s in zip(lengths, row)))
3617
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003618 @staticmethod
3619 def _parse_identity(identity):
3620 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3621 username, domain = identity.split('.', 1)
3622 if username.startswith('git-'):
3623 username = username[len('git-'):]
3624 return username, domain
3625
3626 def _get_usernames_of_domain(self, domain):
3627 """Returns list of usernames referenced by .gitcookies in a given domain."""
3628 identities_by_domain = {}
3629 for _, identity, _ in self.get_hosts_with_creds():
3630 username, domain = self._parse_identity(identity)
3631 identities_by_domain.setdefault(domain, []).append(username)
3632 return identities_by_domain.get(domain)
3633
3634 def _canonical_git_googlesource_host(self, host):
3635 """Normalizes Gerrit hosts (with '-review') to Git host."""
3636 assert host.endswith(self._GOOGLESOURCE)
3637 # Prefix doesn't include '.' at the end.
3638 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3639 if prefix.endswith('-review'):
3640 prefix = prefix[:-len('-review')]
3641 return prefix + '.' + self._GOOGLESOURCE
3642
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003643 def _canonical_gerrit_googlesource_host(self, host):
3644 git_host = self._canonical_git_googlesource_host(host)
3645 prefix = git_host.split('.', 1)[0]
3646 return prefix + '-review.' + self._GOOGLESOURCE
3647
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003648 def has_generic_host(self):
3649 """Returns whether generic .googlesource.com has been configured.
3650
3651 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3652 """
3653 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3654 if host == '.' + self._GOOGLESOURCE:
3655 return True
3656 return False
3657
3658 def _get_git_gerrit_identity_pairs(self):
3659 """Returns map from canonic host to pair of identities (Git, Gerrit).
3660
3661 One of identities might be None, meaning not configured.
3662 """
3663 host_to_identity_pairs = {}
3664 for host, identity, _ in self.get_hosts_with_creds():
3665 canonical = self._canonical_git_googlesource_host(host)
3666 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3667 idx = 0 if canonical == host else 1
3668 pair[idx] = identity
3669 return host_to_identity_pairs
3670
3671 def get_partially_configured_hosts(self):
3672 return set(
3673 host for host, identities_pair in
3674 self._get_git_gerrit_identity_pairs().iteritems()
3675 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3676
3677 def get_conflicting_hosts(self):
3678 return set(
3679 host for host, (i1, i2) in
3680 self._get_git_gerrit_identity_pairs().iteritems()
3681 if None not in (i1, i2) and i1 != i2)
3682
3683 def get_duplicated_hosts(self):
3684 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3685 return set(host for host, count in counters.iteritems() if count > 1)
3686
3687 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3688 'chromium.googlesource.com': 'chromium.org',
3689 'chrome-internal.googlesource.com': 'google.com',
3690 }
3691
3692 def get_hosts_with_wrong_identities(self):
3693 """Finds hosts which **likely** reference wrong identities.
3694
3695 Note: skips hosts which have conflicting identities for Git and Gerrit.
3696 """
3697 hosts = set()
3698 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3699 pair = self._get_git_gerrit_identity_pairs().get(host)
3700 if pair and pair[0] == pair[1]:
3701 _, domain = self._parse_identity(pair[0])
3702 if domain != expected:
3703 hosts.add(host)
3704 return hosts
3705
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003706 @staticmethod
3707 def print_hosts(hosts, extra_column_func=None):
3708 hosts = sorted(hosts)
3709 assert hosts
3710 if extra_column_func is None:
3711 extras = [''] * len(hosts)
3712 else:
3713 extras = [extra_column_func(host) for host in hosts]
3714 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3715 for he in zip(hosts, extras):
3716 print(tmpl % he)
3717 print()
3718
3719 def find_and_report_problems(self):
3720 """Returns True if there was at least one problem, else False."""
3721 problems = [False]
3722 def add_problem():
3723 if not problems[0]:
3724 print('.gitcookies problem report:\n')
3725 problems[0] = True
3726
3727 if self.has_generic_host():
3728 add_problem()
3729 print(' .googlesource.com record detected\n'
3730 ' Chrome Infrastructure team recommends to list full host names '
3731 'explicitly.\n')
3732
3733 dups = self.get_duplicated_hosts()
3734 if dups:
3735 add_problem()
3736 print(' The following hosts were defined twice:\n')
3737 self.print_hosts(dups)
3738
3739 partial = self.get_partially_configured_hosts()
3740 if partial:
3741 add_problem()
3742 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3743 'These hosts are missing:')
3744 self.print_hosts(partial)
3745
3746 conflicting = self.get_conflicting_hosts()
3747 if conflicting:
3748 add_problem()
3749 print(' The following Git hosts have differing credentials from their '
3750 'Gerrit counterparts:\n')
3751 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3752 tuple(self._get_git_gerrit_identity_pairs()[host]))
3753
3754 wrong = self.get_hosts_with_wrong_identities()
3755 if wrong:
3756 add_problem()
3757 print(' These hosts likely use wrong identity:\n')
3758 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3759 (self._get_git_gerrit_identity_pairs()[host][0],
3760 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3761 return problems[0]
3762
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003763
3764def CMDcreds_check(parser, args):
3765 """Checks credentials and suggests changes."""
3766 _, _ = parser.parse_args(args)
3767
3768 if gerrit_util.GceAuthenticator.is_gce():
3769 DieWithError('this command is not designed for GCE, are you on a bot?')
3770
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003771 checker = _GitCookiesChecker()
3772 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003773
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003774 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003775 checker.print_current_creds(include_netrc=True)
3776
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003777 if not checker.find_and_report_problems():
3778 print('\nNo problems detected in your .gitcookies')
3779 return 0
3780 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003781
3782
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003783@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003785 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786
tandrii5d0a0422016-09-14 06:24:35 -07003787 print('WARNING: git cl config works for Rietveld only')
3788 # TODO(tandrii): remove this once we switch to Gerrit.
3789 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003790 parser.add_option('--activate-update', action='store_true',
3791 help='activate auto-updating [rietveld] section in '
3792 '.git/config')
3793 parser.add_option('--deactivate-update', action='store_true',
3794 help='deactivate auto-updating [rietveld] section in '
3795 '.git/config')
3796 options, args = parser.parse_args(args)
3797
3798 if options.deactivate_update:
3799 RunGit(['config', 'rietveld.autoupdate', 'false'])
3800 return
3801
3802 if options.activate_update:
3803 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3804 return
3805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003807 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003808 return 0
3809
3810 url = args[0]
3811 if not url.endswith('codereview.settings'):
3812 url = os.path.join(url, 'codereview.settings')
3813
3814 # Load code review settings and download hooks (if available).
3815 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3816 return 0
3817
3818
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003819def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003820 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003821 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3822 branch = ShortBranchName(branchref)
3823 _, args = parser.parse_args(args)
3824 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003825 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003826 return RunGit(['config', 'branch.%s.base-url' % branch],
3827 error_ok=False).strip()
3828 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003829 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003830 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3831 error_ok=False).strip()
3832
3833
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003834def color_for_status(status):
3835 """Maps a Changelist status to color, for CMDstatus and other tools."""
3836 return {
3837 'unsent': Fore.RED,
3838 'waiting': Fore.BLUE,
3839 'reply': Fore.YELLOW,
3840 'lgtm': Fore.GREEN,
3841 'commit': Fore.MAGENTA,
3842 'closed': Fore.CYAN,
3843 'error': Fore.WHITE,
3844 }.get(status, Fore.WHITE)
3845
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003846
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003847def get_cl_statuses(changes, fine_grained, max_processes=None):
3848 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003849
3850 If fine_grained is true, this will fetch CL statuses from the server.
3851 Otherwise, simply indicate if there's a matching url for the given branches.
3852
3853 If max_processes is specified, it is used as the maximum number of processes
3854 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3855 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003856
3857 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003858 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003859 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003860 upload.verbosity = 0
3861
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003862 if not changes:
3863 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003864
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003865 if not fine_grained:
3866 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003867 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003868 for cl in changes:
3869 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003870 return
3871
3872 # First, sort out authentication issues.
3873 logging.debug('ensuring credentials exist')
3874 for cl in changes:
3875 cl.EnsureAuthenticated(force=False, refresh=True)
3876
3877 def fetch(cl):
3878 try:
3879 return (cl, cl.GetStatus())
3880 except:
3881 # See http://crbug.com/629863.
3882 logging.exception('failed to fetch status for %s:', cl)
3883 raise
3884
3885 threads_count = len(changes)
3886 if max_processes:
3887 threads_count = max(1, min(threads_count, max_processes))
3888 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3889
3890 pool = ThreadPool(threads_count)
3891 fetched_cls = set()
3892 try:
3893 it = pool.imap_unordered(fetch, changes).__iter__()
3894 while True:
3895 try:
3896 cl, status = it.next(timeout=5)
3897 except multiprocessing.TimeoutError:
3898 break
3899 fetched_cls.add(cl)
3900 yield cl, status
3901 finally:
3902 pool.close()
3903
3904 # Add any branches that failed to fetch.
3905 for cl in set(changes) - fetched_cls:
3906 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003907
rmistry@google.com2dd99862015-06-22 12:22:18 +00003908
3909def upload_branch_deps(cl, args):
3910 """Uploads CLs of local branches that are dependents of the current branch.
3911
3912 If the local branch dependency tree looks like:
3913 test1 -> test2.1 -> test3.1
3914 -> test3.2
3915 -> test2.2 -> test3.3
3916
3917 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3918 run on the dependent branches in this order:
3919 test2.1, test3.1, test3.2, test2.2, test3.3
3920
3921 Note: This function does not rebase your local dependent branches. Use it when
3922 you make a change to the parent branch that will not conflict with its
3923 dependent branches, and you would like their dependencies updated in
3924 Rietveld.
3925 """
3926 if git_common.is_dirty_git_tree('upload-branch-deps'):
3927 return 1
3928
3929 root_branch = cl.GetBranch()
3930 if root_branch is None:
3931 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3932 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003933 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003934 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3935 'patchset dependencies without an uploaded CL.')
3936
3937 branches = RunGit(['for-each-ref',
3938 '--format=%(refname:short) %(upstream:short)',
3939 'refs/heads'])
3940 if not branches:
3941 print('No local branches found.')
3942 return 0
3943
3944 # Create a dictionary of all local branches to the branches that are dependent
3945 # on it.
3946 tracked_to_dependents = collections.defaultdict(list)
3947 for b in branches.splitlines():
3948 tokens = b.split()
3949 if len(tokens) == 2:
3950 branch_name, tracked = tokens
3951 tracked_to_dependents[tracked].append(branch_name)
3952
vapiera7fbd5a2016-06-16 09:17:49 -07003953 print()
3954 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003955 dependents = []
3956 def traverse_dependents_preorder(branch, padding=''):
3957 dependents_to_process = tracked_to_dependents.get(branch, [])
3958 padding += ' '
3959 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003960 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003961 dependents.append(dependent)
3962 traverse_dependents_preorder(dependent, padding)
3963 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003965
3966 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003967 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003968 return 0
3969
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003970 confirm_or_exit('This command will checkout all dependent branches and run '
3971 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003972
andybons@chromium.org962f9462016-02-03 20:00:42 +00003973 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003974 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003975 args.extend(['-t', 'Updated patchset dependency'])
3976
rmistry@google.com2dd99862015-06-22 12:22:18 +00003977 # Record all dependents that failed to upload.
3978 failures = {}
3979 # Go through all dependents, checkout the branch and upload.
3980 try:
3981 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003982 print()
3983 print('--------------------------------------')
3984 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003985 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003987 try:
3988 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003990 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003991 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003992 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003993 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003994 finally:
3995 # Swap back to the original root branch.
3996 RunGit(['checkout', '-q', root_branch])
3997
vapiera7fbd5a2016-06-16 09:17:49 -07003998 print()
3999 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000 for dependent_branch in dependents:
4001 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004002 print(' %s : %s' % (dependent_branch, upload_status))
4003 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004004
4005 return 0
4006
4007
kmarshall3bff56b2016-06-06 18:31:47 -07004008def CMDarchive(parser, args):
4009 """Archives and deletes branches associated with closed changelists."""
4010 parser.add_option(
4011 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004012 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004013 parser.add_option(
4014 '-f', '--force', action='store_true',
4015 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004016 parser.add_option(
4017 '-d', '--dry-run', action='store_true',
4018 help='Skip the branch tagging and removal steps.')
4019 parser.add_option(
4020 '-t', '--notags', action='store_true',
4021 help='Do not tag archived branches. '
4022 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004023
4024 auth.add_auth_options(parser)
4025 options, args = parser.parse_args(args)
4026 if args:
4027 parser.error('Unsupported args: %s' % ' '.join(args))
4028 auth_config = auth.extract_auth_config_from_options(options)
4029
4030 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4031 if not branches:
4032 return 0
4033
vapiera7fbd5a2016-06-16 09:17:49 -07004034 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004035 changes = [Changelist(branchref=b, auth_config=auth_config)
4036 for b in branches.splitlines()]
4037 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4038 statuses = get_cl_statuses(changes,
4039 fine_grained=True,
4040 max_processes=options.maxjobs)
4041 proposal = [(cl.GetBranch(),
4042 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4043 for cl, status in statuses
4044 if status == 'closed']
4045 proposal.sort()
4046
4047 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004048 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004049 return 0
4050
4051 current_branch = GetCurrentBranch()
4052
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004054 if options.notags:
4055 for next_item in proposal:
4056 print(' ' + next_item[0])
4057 else:
4058 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4059 for next_item in proposal:
4060 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004061
kmarshall9249e012016-08-23 12:02:16 -07004062 # Quit now on precondition failure or if instructed by the user, either
4063 # via an interactive prompt or by command line flags.
4064 if options.dry_run:
4065 print('\nNo changes were made (dry run).\n')
4066 return 0
4067 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004068 print('You are currently on a branch \'%s\' which is associated with a '
4069 'closed codereview issue, so archive cannot proceed. Please '
4070 'checkout another branch and run this command again.' %
4071 current_branch)
4072 return 1
kmarshall9249e012016-08-23 12:02:16 -07004073 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004074 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4075 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004076 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004077 return 1
4078
4079 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004080 if not options.notags:
4081 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004082 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004083
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004085
4086 return 0
4087
4088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004089def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004090 """Show status of changelists.
4091
4092 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004093 - Red not sent for review or broken
4094 - Blue waiting for review
4095 - Yellow waiting for you to reply to review
4096 - Green LGTM'ed
4097 - Magenta in the commit queue
4098 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004099
4100 Also see 'git cl comments'.
4101 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004103 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004104 parser.add_option('-f', '--fast', action='store_true',
4105 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004106 parser.add_option(
4107 '-j', '--maxjobs', action='store', type=int,
4108 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004109
4110 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004111 _add_codereview_issue_select_options(
4112 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004113 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004114 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004115 if args:
4116 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004117 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118
iannuccie53c9352016-08-17 14:40:40 -07004119 if options.issue is not None and not options.field:
4120 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004122 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004123 cl = Changelist(auth_config=auth_config, issue=options.issue,
4124 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004125 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004127 elif options.field == 'id':
4128 issueid = cl.GetIssue()
4129 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004130 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131 elif options.field == 'patch':
4132 patchset = cl.GetPatchset()
4133 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004134 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004135 elif options.field == 'status':
4136 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004137 elif options.field == 'url':
4138 url = cl.GetIssueURL()
4139 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004141 return 0
4142
4143 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4144 if not branches:
4145 print('No local branch found.')
4146 return 0
4147
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004148 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004149 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004150 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004151 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004152 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004153 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004154 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004155
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004156 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004157 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4158 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4159 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004160 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004161 c, status = output.next()
4162 branch_statuses[c.GetBranch()] = status
4163 status = branch_statuses.pop(branch)
4164 url = cl.GetIssueURL()
4165 if url and (not status or status == 'error'):
4166 # The issue probably doesn't exist anymore.
4167 url += ' (broken)'
4168
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004169 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004170 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004171 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004172 color = ''
4173 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004174 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004176 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004177 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004178
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004179
4180 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004181 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004182 print('Current branch: %s' % branch)
4183 for cl in changes:
4184 if cl.GetBranch() == branch:
4185 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004186 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004187 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004188 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004189 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004190 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print('Issue description:')
4192 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193 return 0
4194
4195
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004196def colorize_CMDstatus_doc():
4197 """To be called once in main() to add colors to git cl status help."""
4198 colors = [i for i in dir(Fore) if i[0].isupper()]
4199
4200 def colorize_line(line):
4201 for color in colors:
4202 if color in line.upper():
4203 # Extract whitespaces first and the leading '-'.
4204 indent = len(line) - len(line.lstrip(' ')) + 1
4205 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4206 return line
4207
4208 lines = CMDstatus.__doc__.splitlines()
4209 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4210
4211
phajdan.jre328cf92016-08-22 04:12:17 -07004212def write_json(path, contents):
4213 with open(path, 'w') as f:
4214 json.dump(contents, f)
4215
4216
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004217@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004219 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220
4221 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004222 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004223 parser.add_option('-r', '--reverse', action='store_true',
4224 help='Lookup the branch(es) for the specified issues. If '
4225 'no issues are specified, all branches with mapped '
4226 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004227 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004228 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004229 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004230 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231
dnj@chromium.org406c4402015-03-03 17:22:28 +00004232 if options.reverse:
4233 branches = RunGit(['for-each-ref', 'refs/heads',
4234 '--format=%(refname:short)']).splitlines()
4235
4236 # Reverse issue lookup.
4237 issue_branch_map = {}
4238 for branch in branches:
4239 cl = Changelist(branchref=branch)
4240 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4241 if not args:
4242 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004243 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004244 for issue in args:
4245 if not issue:
4246 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004247 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print('Branch for issue number %s: %s' % (
4249 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004250 if options.json:
4251 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004252 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004253 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004254 if len(args) > 0:
4255 try:
4256 issue = int(args[0])
4257 except ValueError:
4258 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004259 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004260 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004261 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004262 if options.json:
4263 write_json(options.json, {
4264 'issue': cl.GetIssue(),
4265 'issue_url': cl.GetIssueURL(),
4266 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267 return 0
4268
4269
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004270def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004271 """Shows or posts review comments for any changelist."""
4272 parser.add_option('-a', '--add-comment', dest='comment',
4273 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004274 parser.add_option('-i', '--issue', dest='issue',
4275 help='review issue id (defaults to current issue). '
4276 'If given, requires --rietveld or --gerrit')
smut@google.comc85ac942015-09-15 16:34:43 +00004277 parser.add_option('-j', '--json-file',
4278 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004279 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004280 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004281 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004282 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004283 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004284
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004285 issue = None
4286 if options.issue:
4287 try:
4288 issue = int(options.issue)
4289 except ValueError:
4290 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004291 if not options.forced_codereview:
4292 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004293
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004294 cl = Changelist(issue=issue,
4295 # TODO(tandrii): remove 'rietveld' default.
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01004296 codereview=options.forced_codereview or (
4297 'rietveld' if issue else None),
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004298 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004299
4300 if options.comment:
4301 cl.AddComment(options.comment)
4302 return 0
4303
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004304 summary = sorted(cl.GetCommentsSummary(), key=lambda c: c.date)
4305 for comment in summary:
4306 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004307 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004308 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004309 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004310 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004311 color = Fore.MAGENTA
4312 else:
4313 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004314 print('\n%s%s %s%s\n%s' % (
4315 color,
4316 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4317 comment.sender,
4318 Fore.RESET,
4319 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4320
smut@google.comc85ac942015-09-15 16:34:43 +00004321 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004322 def pre_serialize(c):
4323 dct = c.__dict__.copy()
4324 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4325 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004326 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004327 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004328 return 0
4329
4330
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004331@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004332def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004333 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004334 parser.add_option('-d', '--display', action='store_true',
4335 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004336 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004337 help='New description to set for this issue (- for stdin, '
4338 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004339 parser.add_option('-f', '--force', action='store_true',
4340 help='Delete any unpublished Gerrit edits for this issue '
4341 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004342
4343 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004344 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004345 options, args = parser.parse_args(args)
4346 _process_codereview_select_options(parser, options)
4347
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004348 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004349 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004350 target_issue_arg = ParseIssueNumberArgument(args[0])
4351 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004352 parser.print_help()
4353 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004354
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004355 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004356
martiniss6eda05f2016-06-30 10:18:35 -07004357 kwargs = {
4358 'auth_config': auth_config,
4359 'codereview': options.forced_codereview,
4360 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004361 if target_issue_arg:
4362 kwargs['issue'] = target_issue_arg.issue
4363 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004364
4365 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004366
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004367 if not cl.GetIssue():
4368 DieWithError('This branch has no associated changelist.')
4369 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004370
smut@google.com34fb6b12015-07-13 20:03:26 +00004371 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004372 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004373 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004374
4375 if options.new_description:
4376 text = options.new_description
4377 if text == '-':
4378 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004379 elif text == '+':
4380 base_branch = cl.GetCommonAncestorWithUpstream()
4381 change = cl.GetChange(base_branch, None, local_description=True)
4382 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004383
4384 description.set_description(text)
4385 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004386 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004387
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004388 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004389 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004390 return 0
4391
4392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004393def CreateDescriptionFromLog(args):
4394 """Pulls out the commit log to use as a base for the CL description."""
4395 log_args = []
4396 if len(args) == 1 and not args[0].endswith('.'):
4397 log_args = [args[0] + '..']
4398 elif len(args) == 1 and args[0].endswith('...'):
4399 log_args = [args[0][:-1]]
4400 elif len(args) == 2:
4401 log_args = [args[0] + '..' + args[1]]
4402 else:
4403 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004404 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004405
4406
thestig@chromium.org44202a22014-03-11 19:22:18 +00004407def CMDlint(parser, args):
4408 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004409 parser.add_option('--filter', action='append', metavar='-x,+y',
4410 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004411 auth.add_auth_options(parser)
4412 options, args = parser.parse_args(args)
4413 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004414
4415 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004416 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004417 try:
4418 import cpplint
4419 import cpplint_chromium
4420 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004421 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004422 return 1
4423
4424 # Change the current working directory before calling lint so that it
4425 # shows the correct base.
4426 previous_cwd = os.getcwd()
4427 os.chdir(settings.GetRoot())
4428 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004429 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004430 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4431 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004432 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004433 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004434 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004435
4436 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004437 command = args + files
4438 if options.filter:
4439 command = ['--filter=' + ','.join(options.filter)] + command
4440 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004441
4442 white_regex = re.compile(settings.GetLintRegex())
4443 black_regex = re.compile(settings.GetLintIgnoreRegex())
4444 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4445 for filename in filenames:
4446 if white_regex.match(filename):
4447 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004449 else:
4450 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4451 extra_check_functions)
4452 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004453 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004454 finally:
4455 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004456 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004457 if cpplint._cpplint_state.error_count != 0:
4458 return 1
4459 return 0
4460
4461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004463 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004464 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004465 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004466 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004467 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004468 auth.add_auth_options(parser)
4469 options, args = parser.parse_args(args)
4470 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004471
sbc@chromium.org71437c02015-04-09 19:29:40 +00004472 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004473 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004474 return 1
4475
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004476 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004477 if args:
4478 base_branch = args[0]
4479 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004480 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004481 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004483 cl.RunHook(
4484 committing=not options.upload,
4485 may_prompt=False,
4486 verbose=options.verbose,
4487 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004488 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004489
4490
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004491def GenerateGerritChangeId(message):
4492 """Returns Ixxxxxx...xxx change id.
4493
4494 Works the same way as
4495 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4496 but can be called on demand on all platforms.
4497
4498 The basic idea is to generate git hash of a state of the tree, original commit
4499 message, author/committer info and timestamps.
4500 """
4501 lines = []
4502 tree_hash = RunGitSilent(['write-tree'])
4503 lines.append('tree %s' % tree_hash.strip())
4504 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4505 if code == 0:
4506 lines.append('parent %s' % parent.strip())
4507 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4508 lines.append('author %s' % author.strip())
4509 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4510 lines.append('committer %s' % committer.strip())
4511 lines.append('')
4512 # Note: Gerrit's commit-hook actually cleans message of some lines and
4513 # whitespace. This code is not doing this, but it clearly won't decrease
4514 # entropy.
4515 lines.append(message)
4516 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4517 stdin='\n'.join(lines))
4518 return 'I%s' % change_hash.strip()
4519
4520
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004521def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004522 """Computes the remote branch ref to use for the CL.
4523
4524 Args:
4525 remote (str): The git remote for the CL.
4526 remote_branch (str): The git remote branch for the CL.
4527 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004528 """
4529 if not (remote and remote_branch):
4530 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004531
wittman@chromium.org455dc922015-01-26 20:15:50 +00004532 if target_branch:
4533 # Cannonicalize branch references to the equivalent local full symbolic
4534 # refs, which are then translated into the remote full symbolic refs
4535 # below.
4536 if '/' not in target_branch:
4537 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4538 else:
4539 prefix_replacements = (
4540 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4541 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4542 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4543 )
4544 match = None
4545 for regex, replacement in prefix_replacements:
4546 match = re.search(regex, target_branch)
4547 if match:
4548 remote_branch = target_branch.replace(match.group(0), replacement)
4549 break
4550 if not match:
4551 # This is a branch path but not one we recognize; use as-is.
4552 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004553 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4554 # Handle the refs that need to land in different refs.
4555 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004556
wittman@chromium.org455dc922015-01-26 20:15:50 +00004557 # Create the true path to the remote branch.
4558 # Does the following translation:
4559 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4560 # * refs/remotes/origin/master -> refs/heads/master
4561 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4562 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4563 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4564 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4565 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4566 'refs/heads/')
4567 elif remote_branch.startswith('refs/remotes/branch-heads'):
4568 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004569
wittman@chromium.org455dc922015-01-26 20:15:50 +00004570 return remote_branch
4571
4572
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004573def cleanup_list(l):
4574 """Fixes a list so that comma separated items are put as individual items.
4575
4576 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4577 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4578 """
4579 items = sum((i.split(',') for i in l), [])
4580 stripped_items = (i.strip() for i in items)
4581 return sorted(filter(None, stripped_items))
4582
4583
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004584@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004585def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004586 """Uploads the current changelist to codereview.
4587
4588 Can skip dependency patchset uploads for a branch by running:
4589 git config branch.branch_name.skip-deps-uploads True
4590 To unset run:
4591 git config --unset branch.branch_name.skip-deps-uploads
4592 Can also set the above globally by using the --global flag.
4593 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004594 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4595 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004596 parser.add_option('--bypass-watchlists', action='store_true',
4597 dest='bypass_watchlists',
4598 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004599 parser.add_option('-f', action='store_true', dest='force',
4600 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004601 parser.add_option('--message', '-m', dest='message',
4602 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004603 parser.add_option('-b', '--bug',
4604 help='pre-populate the bug number(s) for this issue. '
4605 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004606 parser.add_option('--message-file', dest='message_file',
4607 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004608 parser.add_option('--title', '-t', dest='title',
4609 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004610 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004611 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004612 help='reviewer email addresses')
4613 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004614 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004615 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004616 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004617 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004618 parser.add_option('--emulate_svn_auto_props',
4619 '--emulate-svn-auto-props',
4620 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004621 dest="emulate_svn_auto_props",
4622 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004623 parser.add_option('-c', '--use-commit-queue', action='store_true',
4624 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004625 parser.add_option('--private', action='store_true',
4626 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004627 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004628 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004629 metavar='TARGET',
4630 help='Apply CL to remote ref TARGET. ' +
4631 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004632 parser.add_option('--squash', action='store_true',
4633 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004634 parser.add_option('--no-squash', action='store_true',
4635 help='Don\'t squash multiple commits into one ' +
4636 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004637 parser.add_option('--topic', default=None,
4638 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004639 parser.add_option('--email', default=None,
4640 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004641 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4642 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004643 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4644 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004645 help='Send the patchset to do a CQ dry run right after '
4646 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004647 parser.add_option('--dependencies', action='store_true',
4648 help='Uploads CLs of all the local branches that depend on '
4649 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004650
rmistry@google.com2dd99862015-06-22 12:22:18 +00004651 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004652 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004653 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004654 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004655 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004656 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004657 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004658
sbc@chromium.org71437c02015-04-09 19:29:40 +00004659 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004660 return 1
4661
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004662 options.reviewers = cleanup_list(options.reviewers)
4663 options.cc = cleanup_list(options.cc)
4664
tandriib80458a2016-06-23 12:20:07 -07004665 if options.message_file:
4666 if options.message:
4667 parser.error('only one of --message and --message-file allowed.')
4668 options.message = gclient_utils.FileRead(options.message_file)
4669 options.message_file = None
4670
tandrii4d0545a2016-07-06 03:56:49 -07004671 if options.cq_dry_run and options.use_commit_queue:
4672 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4673
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004674 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4675 settings.GetIsGerrit()
4676
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004677 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004678 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004679
4680
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004681@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004683 """DEPRECATED: Used to commit the current changelist via git-svn."""
4684 message = ('git-cl no longer supports committing to SVN repositories via '
4685 'git-svn. You probably want to use `git cl land` instead.')
4686 print(message)
4687 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004688
4689
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004690@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004691def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004692 """Commits the current changelist via git.
4693
4694 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4695 upstream and closes the issue automatically and atomically.
4696
4697 Otherwise (in case of Rietveld):
4698 Squashes branch into a single commit.
4699 Updates commit message with metadata (e.g. pointer to review).
4700 Pushes the code upstream.
4701 Updates review and closes.
4702 """
4703 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4704 help='bypass upload presubmit hook')
4705 parser.add_option('-m', dest='message',
4706 help="override review description")
4707 parser.add_option('-f', action='store_true', dest='force',
4708 help="force yes to questions (don't prompt)")
4709 parser.add_option('-c', dest='contributor',
4710 help="external contributor for patch (appended to " +
4711 "description and used as author for git). Should be " +
4712 "formatted as 'First Last <email@example.com>'")
4713 add_git_similarity(parser)
4714 auth.add_auth_options(parser)
4715 (options, args) = parser.parse_args(args)
4716 auth_config = auth.extract_auth_config_from_options(options)
4717
4718 cl = Changelist(auth_config=auth_config)
4719
4720 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4721 if cl.IsGerrit():
4722 if options.message:
4723 # This could be implemented, but it requires sending a new patch to
4724 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4725 # Besides, Gerrit has the ability to change the commit message on submit
4726 # automatically, thus there is no need to support this option (so far?).
4727 parser.error('-m MESSAGE option is not supported for Gerrit.')
4728 if options.contributor:
4729 parser.error(
4730 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4731 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4732 'the contributor\'s "name <email>". If you can\'t upload such a '
4733 'commit for review, contact your repository admin and request'
4734 '"Forge-Author" permission.')
4735 if not cl.GetIssue():
4736 DieWithError('You must upload the change first to Gerrit.\n'
4737 ' If you would rather have `git cl land` upload '
4738 'automatically for you, see http://crbug.com/642759')
4739 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4740 options.verbose)
4741
4742 current = cl.GetBranch()
4743 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4744 if remote == '.':
4745 print()
4746 print('Attempting to push branch %r into another local branch!' % current)
4747 print()
4748 print('Either reparent this branch on top of origin/master:')
4749 print(' git reparent-branch --root')
4750 print()
4751 print('OR run `git rebase-update` if you think the parent branch is ')
4752 print('already committed.')
4753 print()
4754 print(' Current parent: %r' % upstream_branch)
4755 return 1
4756
4757 if not args:
4758 # Default to merging against our best guess of the upstream branch.
4759 args = [cl.GetUpstreamBranch()]
4760
4761 if options.contributor:
4762 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4763 print("Please provide contibutor as 'First Last <email@example.com>'")
4764 return 1
4765
4766 base_branch = args[0]
4767
4768 if git_common.is_dirty_git_tree('land'):
4769 return 1
4770
4771 # This rev-list syntax means "show all commits not in my branch that
4772 # are in base_branch".
4773 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4774 base_branch]).splitlines()
4775 if upstream_commits:
4776 print('Base branch "%s" has %d commits '
4777 'not in this branch.' % (base_branch, len(upstream_commits)))
4778 print('Run "git merge %s" before attempting to land.' % base_branch)
4779 return 1
4780
4781 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4782 if not options.bypass_hooks:
4783 author = None
4784 if options.contributor:
4785 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4786 hook_results = cl.RunHook(
4787 committing=True,
4788 may_prompt=not options.force,
4789 verbose=options.verbose,
4790 change=cl.GetChange(merge_base, author))
4791 if not hook_results.should_continue():
4792 return 1
4793
4794 # Check the tree status if the tree status URL is set.
4795 status = GetTreeStatus()
4796 if 'closed' == status:
4797 print('The tree is closed. Please wait for it to reopen. Use '
4798 '"git cl land --bypass-hooks" to commit on a closed tree.')
4799 return 1
4800 elif 'unknown' == status:
4801 print('Unable to determine tree status. Please verify manually and '
4802 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4803 return 1
4804
4805 change_desc = ChangeDescription(options.message)
4806 if not change_desc.description and cl.GetIssue():
4807 change_desc = ChangeDescription(cl.GetDescription())
4808
4809 if not change_desc.description:
4810 if not cl.GetIssue() and options.bypass_hooks:
4811 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4812 else:
4813 print('No description set.')
4814 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4815 return 1
4816
4817 # Keep a separate copy for the commit message, because the commit message
4818 # contains the link to the Rietveld issue, while the Rietveld message contains
4819 # the commit viewvc url.
4820 if cl.GetIssue():
4821 change_desc.update_reviewers(cl.GetApprovingReviewers())
4822
4823 commit_desc = ChangeDescription(change_desc.description)
4824 if cl.GetIssue():
4825 # Xcode won't linkify this URL unless there is a non-whitespace character
4826 # after it. Add a period on a new line to circumvent this. Also add a space
4827 # before the period to make sure that Gitiles continues to correctly resolve
4828 # the URL.
4829 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4830 if options.contributor:
4831 commit_desc.append_footer('Patch from %s.' % options.contributor)
4832
4833 print('Description:')
4834 print(commit_desc.description)
4835
4836 branches = [merge_base, cl.GetBranchRef()]
4837 if not options.force:
4838 print_stats(options.similarity, options.find_copies, branches)
4839
4840 # We want to squash all this branch's commits into one commit with the proper
4841 # description. We do this by doing a "reset --soft" to the base branch (which
4842 # keeps the working copy the same), then landing that.
4843 MERGE_BRANCH = 'git-cl-commit'
4844 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4845 # Delete the branches if they exist.
4846 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4847 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4848 result = RunGitWithCode(showref_cmd)
4849 if result[0] == 0:
4850 RunGit(['branch', '-D', branch])
4851
4852 # We might be in a directory that's present in this branch but not in the
4853 # trunk. Move up to the top of the tree so that git commands that expect a
4854 # valid CWD won't fail after we check out the merge branch.
4855 rel_base_path = settings.GetRelativeRoot()
4856 if rel_base_path:
4857 os.chdir(rel_base_path)
4858
4859 # Stuff our change into the merge branch.
4860 # We wrap in a try...finally block so if anything goes wrong,
4861 # we clean up the branches.
4862 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004863 revision = None
4864 try:
4865 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4866 RunGit(['reset', '--soft', merge_base])
4867 if options.contributor:
4868 RunGit(
4869 [
4870 'commit', '--author', options.contributor,
4871 '-m', commit_desc.description,
4872 ])
4873 else:
4874 RunGit(['commit', '-m', commit_desc.description])
4875
4876 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4877 mirror = settings.GetGitMirror(remote)
4878 if mirror:
4879 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004880 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004881 else:
4882 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004883 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004884 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4885
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004886 if git_numberer_enabled:
4887 # TODO(tandrii): maybe do autorebase + retry on failure
4888 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004889 logging.debug('Adding git number footers')
4890 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4891 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4892 branch)
4893 # Ensure timestamps are monotonically increasing.
4894 timestamp = max(1 + _get_committer_timestamp(merge_base),
4895 _get_committer_timestamp('HEAD'))
4896 _git_amend_head(commit_desc.description, timestamp)
4897 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004898
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004899 retcode, output = RunGitWithCode(
4900 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004901 if retcode == 0:
4902 revision = RunGit(['rev-parse', 'HEAD']).strip()
4903 logging.debug(output)
4904 except: # pylint: disable=bare-except
4905 if _IS_BEING_TESTED:
4906 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4907 + '-' * 30 + '8<' + '-' * 30)
4908 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4909 raise
4910 finally:
4911 # And then swap back to the original branch and clean up.
4912 RunGit(['checkout', '-q', cl.GetBranch()])
4913 RunGit(['branch', '-D', MERGE_BRANCH])
4914
4915 if not revision:
4916 print('Failed to push. If this persists, please file a bug.')
4917 return 1
4918
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004919 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004920 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004921 if viewvc_url and revision:
4922 change_desc.append_footer(
4923 'Committed: %s%s' % (viewvc_url, revision))
4924 elif revision:
4925 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004926 print('Closing issue '
4927 '(you may be prompted for your codereview password)...')
4928 cl.UpdateDescription(change_desc.description)
4929 cl.CloseIssue()
4930 props = cl.GetIssueProperties()
4931 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004932 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4933 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004934 if options.bypass_hooks:
4935 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4936 else:
4937 comment += ' (presubmit successful).'
4938 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4939
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004940 if os.path.isfile(POSTUPSTREAM_HOOK):
4941 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4942
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004943 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004944
4945
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004946@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004947def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004948 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004949 parser.add_option('-b', dest='newbranch',
4950 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004951 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004952 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004953 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4954 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004955 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004956 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004957 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004958 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004959 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004960 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004961
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004962
4963 group = optparse.OptionGroup(
4964 parser,
4965 'Options for continuing work on the current issue uploaded from a '
4966 'different clone (e.g. different machine). Must be used independently '
4967 'from the other options. No issue number should be specified, and the '
4968 'branch must have an issue number associated with it')
4969 group.add_option('--reapply', action='store_true', dest='reapply',
4970 help='Reset the branch and reapply the issue.\n'
4971 'CAUTION: This will undo any local changes in this '
4972 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004973
4974 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004975 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004976 parser.add_option_group(group)
4977
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004978 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004979 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004980 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004981 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004982 auth_config = auth.extract_auth_config_from_options(options)
4983
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004984
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004985 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004986 if options.newbranch:
4987 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004988 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004989 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004990
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004991 cl = Changelist(auth_config=auth_config,
4992 codereview=options.forced_codereview)
4993 if not cl.GetIssue():
4994 parser.error('current branch must have an associated issue')
4995
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004996 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004997 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004998 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004999
5000 RunGit(['reset', '--hard', upstream])
5001 if options.pull:
5002 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005003
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005004 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5005 options.directory)
5006
5007 if len(args) != 1 or not args[0]:
5008 parser.error('Must specify issue number or url')
5009
5010 # We don't want uncommitted changes mixed up with the patch.
5011 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005012 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005013
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005014 if options.newbranch:
5015 if options.force:
5016 RunGit(['branch', '-D', options.newbranch],
5017 stderr=subprocess2.PIPE, error_ok=True)
5018 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07005019 elif not GetCurrentBranch():
5020 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005021
5022 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
5023
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005024 if cl.IsGerrit():
5025 if options.reject:
5026 parser.error('--reject is not supported with Gerrit codereview.')
5027 if options.nocommit:
5028 parser.error('--nocommit is not supported with Gerrit codereview.')
5029 if options.directory:
5030 parser.error('--directory is not supported with Gerrit codereview.')
5031
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005032 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005033 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005034
5035
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005036def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005037 """Fetches the tree status and returns either 'open', 'closed',
5038 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005039 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005040 if url:
5041 status = urllib2.urlopen(url).read().lower()
5042 if status.find('closed') != -1 or status == '0':
5043 return 'closed'
5044 elif status.find('open') != -1 or status == '1':
5045 return 'open'
5046 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005047 return 'unset'
5048
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005050def GetTreeStatusReason():
5051 """Fetches the tree status from a json url and returns the message
5052 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005053 url = settings.GetTreeStatusUrl()
5054 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005055 connection = urllib2.urlopen(json_url)
5056 status = json.loads(connection.read())
5057 connection.close()
5058 return status['message']
5059
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005061def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005062 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005063 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005064 status = GetTreeStatus()
5065 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005066 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005067 return 2
5068
vapiera7fbd5a2016-06-16 09:17:49 -07005069 print('The tree is %s' % status)
5070 print()
5071 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005072 if status != 'open':
5073 return 1
5074 return 0
5075
5076
maruel@chromium.org15192402012-09-06 12:38:29 +00005077def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005078 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005079 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005080 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005081 '-b', '--bot', action='append',
5082 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5083 'times to specify multiple builders. ex: '
5084 '"-b win_rel -b win_layout". See '
5085 'the try server waterfall for the builders name and the tests '
5086 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005087 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005088 '-B', '--bucket', default='',
5089 help=('Buildbucket bucket to send the try requests.'))
5090 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005091 '-m', '--master', default='',
5092 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005093 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005094 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005095 help='Revision to use for the try job; default: the revision will '
5096 'be determined by the try recipe that builder runs, which usually '
5097 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005098 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005099 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005100 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005101 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005102 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005103 '--project',
5104 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005105 'in recipe to determine to which repository or directory to '
5106 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005107 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005108 '-p', '--property', dest='properties', action='append', default=[],
5109 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005110 'key2=value2 etc. The value will be treated as '
5111 'json if decodable, or as string otherwise. '
5112 'NOTE: using this may make your try job not usable for CQ, '
5113 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005114 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005115 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5116 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005117 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005118 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005119 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005120 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005121
machenbach@chromium.org45453142015-09-15 08:45:22 +00005122 # Make sure that all properties are prop=value pairs.
5123 bad_params = [x for x in options.properties if '=' not in x]
5124 if bad_params:
5125 parser.error('Got properties with missing "=": %s' % bad_params)
5126
maruel@chromium.org15192402012-09-06 12:38:29 +00005127 if args:
5128 parser.error('Unknown arguments: %s' % args)
5129
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005130 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005131 if not cl.GetIssue():
5132 parser.error('Need to upload first')
5133
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005134 if cl.IsGerrit():
5135 # HACK: warm up Gerrit change detail cache to save on RPCs.
5136 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5137
tandriie113dfd2016-10-11 10:20:12 -07005138 error_message = cl.CannotTriggerTryJobReason()
5139 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005140 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005141
borenet6c0efe62016-10-19 08:13:29 -07005142 if options.bucket and options.master:
5143 parser.error('Only one of --bucket and --master may be used.')
5144
qyearsley1fdfcb62016-10-24 13:22:03 -07005145 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005146
qyearsleydd49f942016-10-28 11:57:22 -07005147 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5148 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005149 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005150 if options.verbose:
5151 print('git cl try with no bots now defaults to CQ Dry Run.')
5152 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005153
borenet6c0efe62016-10-19 08:13:29 -07005154 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005155 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005156 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005157 'of bot requires an initial job from a parent (usually a builder). '
5158 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005159 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005160 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005161
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005162 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005163 # TODO(tandrii): Checking local patchset against remote patchset is only
5164 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5165 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005166 print('Warning: Codereview server has newer patchsets (%s) than most '
5167 'recent upload from local checkout (%s). Did a previous upload '
5168 'fail?\n'
5169 'By default, git cl try uses the latest patchset from '
5170 'codereview, continuing to use patchset %s.\n' %
5171 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005172
tandrii568043b2016-10-11 07:49:18 -07005173 try:
borenet6c0efe62016-10-19 08:13:29 -07005174 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5175 patchset)
tandrii568043b2016-10-11 07:49:18 -07005176 except BuildbucketResponseException as ex:
5177 print('ERROR: %s' % ex)
5178 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005179 return 0
5180
5181
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005182def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005183 """Prints info about try jobs associated with current CL."""
5184 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005185 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005186 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005187 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005188 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005189 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005190 '--color', action='store_true', default=setup_color.IS_TTY,
5191 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005192 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005193 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5194 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005195 group.add_option(
5196 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005197 parser.add_option_group(group)
5198 auth.add_auth_options(parser)
5199 options, args = parser.parse_args(args)
5200 if args:
5201 parser.error('Unrecognized args: %s' % ' '.join(args))
5202
5203 auth_config = auth.extract_auth_config_from_options(options)
5204 cl = Changelist(auth_config=auth_config)
5205 if not cl.GetIssue():
5206 parser.error('Need to upload first')
5207
tandrii221ab252016-10-06 08:12:04 -07005208 patchset = options.patchset
5209 if not patchset:
5210 patchset = cl.GetMostRecentPatchset()
5211 if not patchset:
5212 parser.error('Codereview doesn\'t know about issue %s. '
5213 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005214 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005215 cl.GetIssue())
5216
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005217 # TODO(tandrii): Checking local patchset against remote patchset is only
5218 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5219 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005220 print('Warning: Codereview server has newer patchsets (%s) than most '
5221 'recent upload from local checkout (%s). Did a previous upload '
5222 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005223 'By default, git cl try-results uses the latest patchset from '
5224 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005225 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005226 try:
tandrii221ab252016-10-06 08:12:04 -07005227 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005228 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005229 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005230 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005231 if options.json:
5232 write_try_results_json(options.json, jobs)
5233 else:
5234 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005235 return 0
5236
5237
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005238@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005239def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005240 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005241 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005242 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005243 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005245 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005246 if args:
5247 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005248 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005249 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005250 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005251 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005252
5253 # Clear configured merge-base, if there is one.
5254 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005255 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005256 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005257 return 0
5258
5259
thestig@chromium.org00858c82013-12-02 23:08:03 +00005260def CMDweb(parser, args):
5261 """Opens the current CL in the web browser."""
5262 _, args = parser.parse_args(args)
5263 if args:
5264 parser.error('Unrecognized args: %s' % ' '.join(args))
5265
5266 issue_url = Changelist().GetIssueURL()
5267 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005268 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005269 return 1
5270
5271 webbrowser.open(issue_url)
5272 return 0
5273
5274
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005275def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005276 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005277 parser.add_option('-d', '--dry-run', action='store_true',
5278 help='trigger in dry run mode')
5279 parser.add_option('-c', '--clear', action='store_true',
5280 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005281 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005282 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005283 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005284 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005285 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005286 if args:
5287 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005288 if options.dry_run and options.clear:
5289 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5290
iannuccie53c9352016-08-17 14:40:40 -07005291 cl = Changelist(auth_config=auth_config, issue=options.issue,
5292 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005293 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005294 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005295 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005296 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005297 state = _CQState.DRY_RUN
5298 else:
5299 state = _CQState.COMMIT
5300 if not cl.GetIssue():
5301 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005302 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005303 return 0
5304
5305
groby@chromium.org411034a2013-02-26 15:12:01 +00005306def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005307 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005308 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005309 auth.add_auth_options(parser)
5310 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005311 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005312 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005313 if args:
5314 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005315 cl = Changelist(auth_config=auth_config, issue=options.issue,
5316 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005317 # Ensure there actually is an issue to close.
5318 cl.GetDescription()
5319 cl.CloseIssue()
5320 return 0
5321
5322
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005323def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005324 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005325 parser.add_option(
5326 '--stat',
5327 action='store_true',
5328 dest='stat',
5329 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005330 auth.add_auth_options(parser)
5331 options, args = parser.parse_args(args)
5332 auth_config = auth.extract_auth_config_from_options(options)
5333 if args:
5334 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005335
5336 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005337 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005338 # Staged changes would be committed along with the patch from last
5339 # upload, hence counted toward the "last upload" side in the final
5340 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005341 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005342 return 1
5343
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005344 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005345 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005346 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005347 if not issue:
5348 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005349 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005350 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005351
5352 # Create a new branch based on the merge-base
5353 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005354 # Clear cached branch in cl object, to avoid overwriting original CL branch
5355 # properties.
5356 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005357 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005358 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005359 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005360 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005361 return rtn
5362
wychen@chromium.org06928532015-02-03 02:11:29 +00005363 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005364 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005365 cmd = ['git', 'diff']
5366 if options.stat:
5367 cmd.append('--stat')
5368 cmd.extend([TMP_BRANCH, branch, '--'])
5369 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005370 finally:
5371 RunGit(['checkout', '-q', branch])
5372 RunGit(['branch', '-D', TMP_BRANCH])
5373
5374 return 0
5375
5376
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005377def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005378 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005379 parser.add_option(
5380 '--no-color',
5381 action='store_true',
5382 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005383 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005384 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005385 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005386
5387 author = RunGit(['config', 'user.email']).strip() or None
5388
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005389 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005390
5391 if args:
5392 if len(args) > 1:
5393 parser.error('Unknown args')
5394 base_branch = args[0]
5395 else:
5396 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005397 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005398
5399 change = cl.GetChange(base_branch, None)
5400 return owners_finder.OwnersFinder(
5401 [f.LocalPath() for f in
5402 cl.GetChange(base_branch, None).AffectedFiles()],
5403 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005404 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005405 disable_color=options.no_color).run()
5406
5407
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005408def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005409 """Generates a diff command."""
5410 # Generate diff for the current branch's changes.
5411 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005412 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005413
5414 if args:
5415 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005416 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005417 diff_cmd.append(arg)
5418 else:
5419 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005420
5421 return diff_cmd
5422
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005423
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005424def MatchingFileType(file_name, extensions):
5425 """Returns true if the file name ends with one of the given extensions."""
5426 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005427
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005428
enne@chromium.org555cfe42014-01-29 18:21:39 +00005429@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005430def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005431 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005432 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005433 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005434 parser.add_option('--full', action='store_true',
5435 help='Reformat the full content of all touched files')
5436 parser.add_option('--dry-run', action='store_true',
5437 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005438 parser.add_option('--python', action='store_true',
5439 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005440 parser.add_option('--js', action='store_true',
5441 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005442 parser.add_option('--diff', action='store_true',
5443 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005444 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005445
Daniel Chengc55eecf2016-12-30 03:11:02 -08005446 # Normalize any remaining args against the current path, so paths relative to
5447 # the current directory are still resolved as expected.
5448 args = [os.path.join(os.getcwd(), arg) for arg in args]
5449
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005450 # git diff generates paths against the root of the repository. Change
5451 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005452 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005453 if rel_base_path:
5454 os.chdir(rel_base_path)
5455
digit@chromium.org29e47272013-05-17 17:01:46 +00005456 # Grab the merge-base commit, i.e. the upstream commit of the current
5457 # branch when it was created or the last time it was rebased. This is
5458 # to cover the case where the user may have called "git fetch origin",
5459 # moving the origin branch to a newer commit, but hasn't rebased yet.
5460 upstream_commit = None
5461 cl = Changelist()
5462 upstream_branch = cl.GetUpstreamBranch()
5463 if upstream_branch:
5464 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5465 upstream_commit = upstream_commit.strip()
5466
5467 if not upstream_commit:
5468 DieWithError('Could not find base commit for this branch. '
5469 'Are you in detached state?')
5470
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005471 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5472 diff_output = RunGit(changed_files_cmd)
5473 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005474 # Filter out files deleted by this CL
5475 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005476
Christopher Lamc5ba6922017-01-24 11:19:14 +11005477 if opts.js:
5478 CLANG_EXTS.append('.js')
5479
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005480 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5481 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5482 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005483 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005484
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005485 top_dir = os.path.normpath(
5486 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5487
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005488 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5489 # formatted. This is used to block during the presubmit.
5490 return_value = 0
5491
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005492 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005493 # Locate the clang-format binary in the checkout
5494 try:
5495 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005496 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005497 DieWithError(e)
5498
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005499 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005500 cmd = [clang_format_tool]
5501 if not opts.dry_run and not opts.diff:
5502 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005503 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005504 if opts.diff:
5505 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005506 else:
5507 env = os.environ.copy()
5508 env['PATH'] = str(os.path.dirname(clang_format_tool))
5509 try:
5510 script = clang_format.FindClangFormatScriptInChromiumTree(
5511 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005512 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005513 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005514
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005515 cmd = [sys.executable, script, '-p0']
5516 if not opts.dry_run and not opts.diff:
5517 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005518
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005519 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5520 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005521
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005522 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5523 if opts.diff:
5524 sys.stdout.write(stdout)
5525 if opts.dry_run and len(stdout) > 0:
5526 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005527
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005528 # Similar code to above, but using yapf on .py files rather than clang-format
5529 # on C/C++ files
5530 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005531 yapf_tool = gclient_utils.FindExecutable('yapf')
5532 if yapf_tool is None:
5533 DieWithError('yapf not found in PATH')
5534
5535 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005536 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005537 cmd = [yapf_tool]
5538 if not opts.dry_run and not opts.diff:
5539 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005540 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005541 if opts.diff:
5542 sys.stdout.write(stdout)
5543 else:
5544 # TODO(sbc): yapf --lines mode still has some issues.
5545 # https://github.com/google/yapf/issues/154
5546 DieWithError('--python currently only works with --full')
5547
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005548 # Dart's formatter does not have the nice property of only operating on
5549 # modified chunks, so hard code full.
5550 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005551 try:
5552 command = [dart_format.FindDartFmtToolInChromiumTree()]
5553 if not opts.dry_run and not opts.diff:
5554 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005555 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005556
ppi@chromium.org6593d932016-03-03 15:41:15 +00005557 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005558 if opts.dry_run and stdout:
5559 return_value = 2
5560 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005561 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5562 'found in this checkout. Files in other languages are still '
5563 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005564
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005565 # Format GN build files. Always run on full build files for canonical form.
5566 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005567 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005568 if opts.dry_run or opts.diff:
5569 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005570 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005571 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5572 shell=sys.platform == 'win32',
5573 cwd=top_dir)
5574 if opts.dry_run and gn_ret == 2:
5575 return_value = 2 # Not formatted.
5576 elif opts.diff and gn_ret == 2:
5577 # TODO this should compute and print the actual diff.
5578 print("This change has GN build file diff for " + gn_diff_file)
5579 elif gn_ret != 0:
5580 # For non-dry run cases (and non-2 return values for dry-run), a
5581 # nonzero error code indicates a failure, probably because the file
5582 # doesn't parse.
5583 DieWithError("gn format failed on " + gn_diff_file +
5584 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005585
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005586 metrics_xml_files = [
Pierre-Antoine Manzagold5bc73d2017-03-22 15:40:40 -04005587 os.path.join('tools', 'metrics', 'actions', 'actions.xml'),
5588 os.path.join('tools', 'metrics', 'histograms', 'histograms.xml'),
5589 os.path.join('tools', 'metrics', 'rappor', 'rappor.xml')]
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005590 for xml_file in metrics_xml_files:
5591 if xml_file in diff_files:
Pierre-Antoine Manzagold5bc73d2017-03-22 15:40:40 -04005592 tool_dir = os.path.join(top_dir, os.path.dirname(xml_file))
5593 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005594 if opts.dry_run or opts.diff:
5595 cmd.append('--diff')
5596 stdout = RunCommand(cmd, cwd=top_dir)
5597 if opts.diff:
5598 sys.stdout.write(stdout)
5599 if opts.dry_run and stdout:
5600 return_value = 2 # Not formatted.
5601
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005602 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005603
5604
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005605@subcommand.usage('<codereview url or issue id>')
5606def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005607 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005608 _, args = parser.parse_args(args)
5609
5610 if len(args) != 1:
5611 parser.print_help()
5612 return 1
5613
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005614 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005615 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005616 parser.print_help()
5617 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005618 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005619
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005620 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005621 output = RunGit(['config', '--local', '--get-regexp',
5622 r'branch\..*\.%s' % issueprefix],
5623 error_ok=True)
5624 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005625 if issue == target_issue:
5626 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005627
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005628 branches = []
5629 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005630 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005631 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005632 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005633 return 1
5634 if len(branches) == 1:
5635 RunGit(['checkout', branches[0]])
5636 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005637 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005638 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005639 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005640 which = raw_input('Choose by index: ')
5641 try:
5642 RunGit(['checkout', branches[int(which)]])
5643 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005644 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005645 return 1
5646
5647 return 0
5648
5649
maruel@chromium.org29404b52014-09-08 22:58:00 +00005650def CMDlol(parser, args):
5651 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005652 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005653 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5654 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5655 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005656 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005657 return 0
5658
5659
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005660class OptionParser(optparse.OptionParser):
5661 """Creates the option parse and add --verbose support."""
5662 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005663 optparse.OptionParser.__init__(
5664 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005665 self.add_option(
5666 '-v', '--verbose', action='count', default=0,
5667 help='Use 2 times for more debugging info')
5668
5669 def parse_args(self, args=None, values=None):
5670 options, args = optparse.OptionParser.parse_args(self, args, values)
5671 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005672 logging.basicConfig(
5673 level=levels[min(options.verbose, len(levels) - 1)],
5674 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5675 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005676 return options, args
5677
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005679def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005680 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005681 print('\nYour python version %s is unsupported, please upgrade.\n' %
5682 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005683 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005684
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005685 # Reload settings.
5686 global settings
5687 settings = Settings()
5688
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005689 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005690 dispatcher = subcommand.CommandDispatcher(__name__)
5691 try:
5692 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005693 except auth.AuthenticationError as e:
5694 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005695 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005696 if e.code != 500:
5697 raise
5698 DieWithError(
5699 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5700 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005701 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005702
5703
5704if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005705 # These affect sys.stdout so do it outside of main() to simplify mocks in
5706 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005707 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005708 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005709 try:
5710 sys.exit(main(sys.argv[1:]))
5711 except KeyboardInterrupt:
5712 sys.stderr.write('interrupted\n')
5713 sys.exit(1)