blob: f001974c7696cb2d037be931b0fac8eb9aec38ba [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 Shyshkalov3e631422017-02-16 17:46:44 +01001543 self._codereview_impl.EnsureCanUploadPatchset()
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 Shyshkalov3e631422017-02-16 17:46:44 +01001812 def EnsureCanUploadPatchset(self):
1813 """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.
1818 """
1819 pass
1820
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001821 def CMDUploadChange(self, options, args, change):
1822 """Uploads a change to codereview."""
1823 raise NotImplementedError()
1824
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001825 def SetCQState(self, new_state):
1826 """Update the CQ state for latest patchset.
1827
1828 Issue must have been already uploaded and known.
1829 """
1830 raise NotImplementedError()
1831
tandriie113dfd2016-10-11 10:20:12 -07001832 def CannotTriggerTryJobReason(self):
1833 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1834 raise NotImplementedError()
1835
tandriide281ae2016-10-12 06:02:30 -07001836 def GetIssueOwner(self):
1837 raise NotImplementedError()
1838
tandrii8c5a3532016-11-04 07:52:02 -07001839 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001840 raise NotImplementedError()
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842
1843class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001844 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845 super(_RietveldChangelistImpl, self).__init__(changelist)
1846 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001847 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001848 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001850 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001851 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852 self._props = None
1853 self._rpc_server = None
1854
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001855 def GetCodereviewServer(self):
1856 if not self._rietveld_server:
1857 # If we're on a branch then get the server potentially associated
1858 # with that branch.
1859 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001860 self._rietveld_server = gclient_utils.UpgradeToHttps(
1861 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001862 if not self._rietveld_server:
1863 self._rietveld_server = settings.GetDefaultServerUrl()
1864 return self._rietveld_server
1865
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001866 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001867 """Best effort check that user is authenticated with Rietveld server."""
1868 if self._auth_config.use_oauth2:
1869 authenticator = auth.get_authenticator_for_host(
1870 self.GetCodereviewServer(), self._auth_config)
1871 if not authenticator.has_cached_credentials():
1872 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001873 if refresh:
1874 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001875
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001876 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 issue = self.GetIssue()
1878 assert issue
1879 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001880 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001881 except urllib2.HTTPError as e:
1882 if e.code == 404:
1883 DieWithError(
1884 ('\nWhile fetching the description for issue %d, received a '
1885 '404 (not found)\n'
1886 'error. It is likely that you deleted this '
1887 'issue on the server. If this is the\n'
1888 'case, please run\n\n'
1889 ' git cl issue 0\n\n'
1890 'to clear the association with the deleted issue. Then run '
1891 'this command again.') % issue)
1892 else:
1893 DieWithError(
1894 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1895 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001896 print('Warning: Failed to retrieve CL description due to network '
1897 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 return ''
1899
1900 def GetMostRecentPatchset(self):
1901 return self.GetIssueProperties()['patchsets'][-1]
1902
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903 def GetIssueProperties(self):
1904 if self._props is None:
1905 issue = self.GetIssue()
1906 if not issue:
1907 self._props = {}
1908 else:
1909 self._props = self.RpcServer().get_issue_properties(issue, True)
1910 return self._props
1911
tandriie113dfd2016-10-11 10:20:12 -07001912 def CannotTriggerTryJobReason(self):
1913 props = self.GetIssueProperties()
1914 if not props:
1915 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1916 if props.get('closed'):
1917 return 'CL %s is closed' % self.GetIssue()
1918 if props.get('private'):
1919 return 'CL %s is private' % self.GetIssue()
1920 return None
1921
tandrii8c5a3532016-11-04 07:52:02 -07001922 def GetTryjobProperties(self, patchset=None):
1923 """Returns dictionary of properties to launch tryjob."""
1924 project = (self.GetIssueProperties() or {}).get('project')
1925 return {
1926 'issue': self.GetIssue(),
1927 'patch_project': project,
1928 'patch_storage': 'rietveld',
1929 'patchset': patchset or self.GetPatchset(),
1930 'rietveld': self.GetCodereviewServer(),
1931 }
1932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001933 def GetApprovingReviewers(self):
1934 return get_approving_reviewers(self.GetIssueProperties())
1935
tandriide281ae2016-10-12 06:02:30 -07001936 def GetIssueOwner(self):
1937 return (self.GetIssueProperties() or {}).get('owner_email')
1938
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939 def AddComment(self, message):
1940 return self.RpcServer().add_comment(self.GetIssue(), message)
1941
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001942 def GetCommentsSummary(self):
1943 summary = []
1944 for message in self.GetIssueProperties().get('messages', []):
1945 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1946 summary.append(_CommentSummary(
1947 date=date,
1948 disapproval=bool(message['disapproval']),
1949 approval=bool(message['approval']),
1950 sender=message['sender'],
1951 message=message['text'],
1952 ))
1953 return summary
1954
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001955 def GetStatus(self):
1956 """Apply a rough heuristic to give a simple summary of an issue's review
1957 or CQ status, assuming adherence to a common workflow.
1958
1959 Returns None if no issue for this branch, or one of the following keywords:
1960 * 'error' - error from review tool (including deleted issues)
1961 * 'unsent' - not sent for review
1962 * 'waiting' - waiting for review
1963 * 'reply' - waiting for owner to reply to review
1964 * 'lgtm' - LGTM from at least one approved reviewer
1965 * 'commit' - in the commit queue
1966 * 'closed' - closed
1967 """
1968 if not self.GetIssue():
1969 return None
1970
1971 try:
1972 props = self.GetIssueProperties()
1973 except urllib2.HTTPError:
1974 return 'error'
1975
1976 if props.get('closed'):
1977 # Issue is closed.
1978 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001979 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001980 # Issue is in the commit queue.
1981 return 'commit'
1982
1983 try:
1984 reviewers = self.GetApprovingReviewers()
1985 except urllib2.HTTPError:
1986 return 'error'
1987
1988 if reviewers:
1989 # Was LGTM'ed.
1990 return 'lgtm'
1991
1992 messages = props.get('messages') or []
1993
tandrii9d2c7a32016-06-22 03:42:45 -07001994 # Skip CQ messages that don't require owner's action.
1995 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1996 if 'Dry run:' in messages[-1]['text']:
1997 messages.pop()
1998 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1999 # This message always follows prior messages from CQ,
2000 # so skip this too.
2001 messages.pop()
2002 else:
2003 # This is probably a CQ messages warranting user attention.
2004 break
2005
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002006 if not messages:
2007 # No message was sent.
2008 return 'unsent'
2009 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002010 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002011 return 'reply'
2012 return 'waiting'
2013
dsansomee2d6fd92016-09-08 00:10:47 -07002014 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002015 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002018 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002020 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002021 return self.SetFlags({flag: value})
2022
2023 def SetFlags(self, flags):
2024 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002025 """
phajdan.jr68598232016-08-10 03:28:28 -07002026 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002027 try:
tandrii4b233bd2016-07-06 03:50:29 -07002028 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002029 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002030 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002031 if e.code == 404:
2032 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2033 if e.code == 403:
2034 DieWithError(
2035 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002036 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002037 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002039 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040 """Returns an upload.RpcServer() to access this review's rietveld instance.
2041 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002042 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002043 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002044 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002045 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002046 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002048 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002049 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002050 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051
tandrii5d48c322016-08-18 16:19:37 -07002052 @classmethod
2053 def PatchsetConfigKey(cls):
2054 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055
tandrii5d48c322016-08-18 16:19:37 -07002056 @classmethod
2057 def CodereviewServerConfigKey(cls):
2058 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002059
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002060 def GetRieveldObjForPresubmit(self):
2061 return self.RpcServer()
2062
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002063 def SetCQState(self, new_state):
2064 props = self.GetIssueProperties()
2065 if props.get('private'):
2066 DieWithError('Cannot set-commit on private issue')
2067
2068 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002069 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002070 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002071 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002072 else:
tandrii4b233bd2016-07-06 03:50:29 -07002073 assert new_state == _CQState.DRY_RUN
2074 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002075
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002076 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2077 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002078 # PatchIssue should never be called with a dirty tree. It is up to the
2079 # caller to check this, but just in case we assert here since the
2080 # consequences of the caller not checking this could be dire.
2081 assert(not git_common.is_dirty_git_tree('apply'))
2082 assert(parsed_issue_arg.valid)
2083 self._changelist.issue = parsed_issue_arg.issue
2084 if parsed_issue_arg.hostname:
2085 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2086
skobes6468b902016-10-24 08:45:10 -07002087 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2088 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2089 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002090 try:
skobes6468b902016-10-24 08:45:10 -07002091 scm_obj.apply_patch(patchset_object)
2092 except Exception as e:
2093 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002094 return 1
2095
2096 # If we had an issue, commit the current state and register the issue.
2097 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002098 self.SetIssue(self.GetIssue())
2099 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002100 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2101 'patch from issue %(i)s at patchset '
2102 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2103 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002104 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002105 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002106 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 return 0
2108
2109 @staticmethod
2110 def ParseIssueURL(parsed_url):
2111 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2112 return None
wychen3c1c1722016-08-04 11:46:36 -07002113 # Rietveld patch: https://domain/<number>/#ps<patchset>
2114 match = re.match(r'/(\d+)/$', parsed_url.path)
2115 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2116 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002117 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002118 issue=int(match.group(1)),
2119 patchset=int(match2.group(1)),
2120 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002121 # Typical url: https://domain/<issue_number>[/[other]]
2122 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2123 if match:
skobes6468b902016-10-24 08:45:10 -07002124 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 issue=int(match.group(1)),
2126 hostname=parsed_url.netloc)
2127 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2128 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2129 if match:
skobes6468b902016-10-24 08:45:10 -07002130 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002131 issue=int(match.group(1)),
2132 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002133 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002134 return None
2135
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002136 def CMDUploadChange(self, options, args, change):
2137 """Upload the patch to Rietveld."""
2138 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2139 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002140 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2141 if options.emulate_svn_auto_props:
2142 upload_args.append('--emulate_svn_auto_props')
2143
2144 change_desc = None
2145
2146 if options.email is not None:
2147 upload_args.extend(['--email', options.email])
2148
2149 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002150 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151 upload_args.extend(['--title', options.title])
2152 if options.message:
2153 upload_args.extend(['--message', options.message])
2154 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002155 print('This branch is associated with issue %s. '
2156 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002157 else:
nodirca166002016-06-27 10:59:51 -07002158 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002159 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002160 if options.message:
2161 message = options.message
2162 else:
2163 message = CreateDescriptionFromLog(args)
2164 if options.title:
2165 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 change_desc = ChangeDescription(message)
2167 if options.reviewers or options.tbr_owners:
2168 change_desc.update_reviewers(options.reviewers,
2169 options.tbr_owners,
2170 change)
2171 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002172 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002173
2174 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002175 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002176 return 1
2177
2178 upload_args.extend(['--message', change_desc.description])
2179 if change_desc.get_reviewers():
2180 upload_args.append('--reviewers=%s' % ','.join(
2181 change_desc.get_reviewers()))
2182 if options.send_mail:
2183 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002184 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002185 upload_args.append('--send_mail')
2186
2187 # We check this before applying rietveld.private assuming that in
2188 # rietveld.cc only addresses which we can send private CLs to are listed
2189 # if rietveld.private is set, and so we should ignore rietveld.cc only
2190 # when --private is specified explicitly on the command line.
2191 if options.private:
2192 logging.warn('rietveld.cc is ignored since private flag is specified. '
2193 'You need to review and add them manually if necessary.')
2194 cc = self.GetCCListWithoutDefault()
2195 else:
2196 cc = self.GetCCList()
2197 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002198 if change_desc.get_cced():
2199 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200 if cc:
2201 upload_args.extend(['--cc', cc])
2202
2203 if options.private or settings.GetDefaultPrivateFlag() == "True":
2204 upload_args.append('--private')
2205
2206 upload_args.extend(['--git_similarity', str(options.similarity)])
2207 if not options.find_copies:
2208 upload_args.extend(['--git_no_find_copies'])
2209
2210 # Include the upstream repo's URL in the change -- this is useful for
2211 # projects that have their source spread across multiple repos.
2212 remote_url = self.GetGitBaseUrlFromConfig()
2213 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002214 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2215 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2216 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002217 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002218 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002219 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 if target_ref:
2221 upload_args.extend(['--target_ref', target_ref])
2222
2223 # Look for dependent patchsets. See crbug.com/480453 for more details.
2224 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2225 upstream_branch = ShortBranchName(upstream_branch)
2226 if remote is '.':
2227 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002228 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002230 print()
2231 print('Skipping dependency patchset upload because git config '
2232 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2233 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002234 else:
2235 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002236 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002237 auth_config=auth_config)
2238 branch_cl_issue_url = branch_cl.GetIssueURL()
2239 branch_cl_issue = branch_cl.GetIssue()
2240 branch_cl_patchset = branch_cl.GetPatchset()
2241 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2242 upload_args.extend(
2243 ['--depends_on_patchset', '%s:%s' % (
2244 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002245 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002246 '\n'
2247 'The current branch (%s) is tracking a local branch (%s) with '
2248 'an associated CL.\n'
2249 'Adding %s/#ps%s as a dependency patchset.\n'
2250 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2251 branch_cl_patchset))
2252
2253 project = settings.GetProject()
2254 if project:
2255 upload_args.extend(['--project', project])
2256
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 try:
2258 upload_args = ['upload'] + upload_args + args
2259 logging.info('upload.RealMain(%s)', upload_args)
2260 issue, patchset = upload.RealMain(upload_args)
2261 issue = int(issue)
2262 patchset = int(patchset)
2263 except KeyboardInterrupt:
2264 sys.exit(1)
2265 except:
2266 # If we got an exception after the user typed a description for their
2267 # change, back up the description before re-raising.
2268 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002269 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 raise
2271
2272 if not self.GetIssue():
2273 self.SetIssue(issue)
2274 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 return 0
2276
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002277
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002278class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002279 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002280 # auth_config is Rietveld thing, kept here to preserve interface only.
2281 super(_GerritChangelistImpl, self).__init__(changelist)
2282 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002283 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002284 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002285 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002286 # Map from change number (issue) to its detail cache.
2287 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002288
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002289 if codereview_host is not None:
2290 assert not codereview_host.startswith('https://'), codereview_host
2291 self._gerrit_host = codereview_host
2292 self._gerrit_server = 'https://%s' % codereview_host
2293
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002294 def _GetGerritHost(self):
2295 # Lazy load of configs.
2296 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002297 if self._gerrit_host and '.' not in self._gerrit_host:
2298 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2299 # This happens for internal stuff http://crbug.com/614312.
2300 parsed = urlparse.urlparse(self.GetRemoteUrl())
2301 if parsed.scheme == 'sso':
2302 print('WARNING: using non https URLs for remote is likely broken\n'
2303 ' Your current remote is: %s' % self.GetRemoteUrl())
2304 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2305 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306 return self._gerrit_host
2307
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002308 def _GetGitHost(self):
2309 """Returns git host to be used when uploading change to Gerrit."""
2310 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2311
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002312 def GetCodereviewServer(self):
2313 if not self._gerrit_server:
2314 # If we're on a branch then get the server potentially associated
2315 # with that branch.
2316 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002317 self._gerrit_server = self._GitGetBranchConfigValue(
2318 self.CodereviewServerConfigKey())
2319 if self._gerrit_server:
2320 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321 if not self._gerrit_server:
2322 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2323 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002324 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002325 parts[0] = parts[0] + '-review'
2326 self._gerrit_host = '.'.join(parts)
2327 self._gerrit_server = 'https://%s' % self._gerrit_host
2328 return self._gerrit_server
2329
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002330 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002331 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002332 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333
tandrii5d48c322016-08-18 16:19:37 -07002334 @classmethod
2335 def PatchsetConfigKey(cls):
2336 return 'gerritpatchset'
2337
2338 @classmethod
2339 def CodereviewServerConfigKey(cls):
2340 return 'gerritserver'
2341
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002342 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002343 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002344 if settings.GetGerritSkipEnsureAuthenticated():
2345 # For projects with unusual authentication schemes.
2346 # See http://crbug.com/603378.
2347 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002348 # Lazy-loader to identify Gerrit and Git hosts.
2349 if gerrit_util.GceAuthenticator.is_gce():
2350 return
2351 self.GetCodereviewServer()
2352 git_host = self._GetGitHost()
2353 assert self._gerrit_server and self._gerrit_host
2354 cookie_auth = gerrit_util.CookiesAuthenticator()
2355
2356 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2357 git_auth = cookie_auth.get_auth_header(git_host)
2358 if gerrit_auth and git_auth:
2359 if gerrit_auth == git_auth:
2360 return
2361 print((
2362 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2363 ' Check your %s or %s file for credentials of hosts:\n'
2364 ' %s\n'
2365 ' %s\n'
2366 ' %s') %
2367 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2368 git_host, self._gerrit_host,
2369 cookie_auth.get_new_password_message(git_host)))
2370 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002371 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002372 return
2373 else:
2374 missing = (
2375 [] if gerrit_auth else [self._gerrit_host] +
2376 [] if git_auth else [git_host])
2377 DieWithError('Credentials for the following hosts are required:\n'
2378 ' %s\n'
2379 'These are read from %s (or legacy %s)\n'
2380 '%s' % (
2381 '\n '.join(missing),
2382 cookie_auth.get_gitcookies_path(),
2383 cookie_auth.get_netrc_path(),
2384 cookie_auth.get_new_password_message(git_host)))
2385
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002386 def EnsureCanUploadPatchset(self):
2387 """Best effort check that uploading isn't supposed to fail for predictable
2388 reasons.
2389
2390 This method should raise informative exception if uploading shouldn't
2391 proceed.
2392 """
2393 if not self.GetIssue():
2394 return
2395
2396 # Warm change details cache now to avoid RPCs later, reducing latency for
2397 # developers.
2398 self.FetchDescription()
2399
2400 status = self._GetChangeDetail()['status']
2401 if status in ('MERGED', 'ABANDONED'):
2402 DieWithError('Change %s has been %s, new uploads are not allowed' %
2403 (self.GetIssueURL(),
2404 'submitted' if status == 'MERGED' else 'abandoned'))
2405
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002406 def _PostUnsetIssueProperties(self):
2407 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002408 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002409
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002410 def GetRieveldObjForPresubmit(self):
2411 class ThisIsNotRietveldIssue(object):
2412 def __nonzero__(self):
2413 # This is a hack to make presubmit_support think that rietveld is not
2414 # defined, yet still ensure that calls directly result in a decent
2415 # exception message below.
2416 return False
2417
2418 def __getattr__(self, attr):
2419 print(
2420 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2421 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2422 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2423 'or use Rietveld for codereview.\n'
2424 'See also http://crbug.com/579160.' % attr)
2425 raise NotImplementedError()
2426 return ThisIsNotRietveldIssue()
2427
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002428 def GetGerritObjForPresubmit(self):
2429 return presubmit_support.GerritAccessor(self._GetGerritHost())
2430
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002431 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002432 """Apply a rough heuristic to give a simple summary of an issue's review
2433 or CQ status, assuming adherence to a common workflow.
2434
2435 Returns None if no issue for this branch, or one of the following keywords:
2436 * 'error' - error from review tool (including deleted issues)
2437 * 'unsent' - no reviewers added
2438 * 'waiting' - waiting for review
2439 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002440 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002441 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002442 * 'commit' - in the commit queue
2443 * 'closed' - abandoned
2444 """
2445 if not self.GetIssue():
2446 return None
2447
2448 try:
2449 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002450 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002451 return 'error'
2452
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002453 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002454 return 'closed'
2455
2456 cq_label = data['labels'].get('Commit-Queue', {})
2457 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002458 votes = cq_label.get('all', [])
2459 highest_vote = 0
2460 for v in votes:
2461 highest_vote = max(highest_vote, v.get('value', 0))
2462 vote_value = str(highest_vote)
2463 if vote_value != '0':
2464 # Add a '+' if the value is not 0 to match the values in the label.
2465 # The cq_label does not have negatives.
2466 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002467 vote_text = cq_label.get('values', {}).get(vote_value, '')
2468 if vote_text.lower() == 'commit':
2469 return 'commit'
2470
2471 lgtm_label = data['labels'].get('Code-Review', {})
2472 if lgtm_label:
2473 if 'rejected' in lgtm_label:
2474 return 'not lgtm'
2475 if 'approved' in lgtm_label:
2476 return 'lgtm'
2477
2478 if not data.get('reviewers', {}).get('REVIEWER', []):
2479 return 'unsent'
2480
2481 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002482 owner = data['owner'].get('_account_id')
2483 while messages:
2484 last_message_author = messages.pop().get('author', {})
2485 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2486 # Ignore replies from CQ.
2487 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002488 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002489 # Some reply from non-owner.
2490 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002491 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002492
2493 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002494 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002495 return data['revisions'][data['current_revision']]['_number']
2496
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002497 def FetchDescription(self, force=False):
2498 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2499 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002500 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002501 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002502
dsansomee2d6fd92016-09-08 00:10:47 -07002503 def UpdateDescriptionRemote(self, description, force=False):
2504 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2505 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002506 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002507 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002508 'unpublished edit. Either publish the edit in the Gerrit web UI '
2509 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002510
2511 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2512 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002513 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002514 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002515
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002516 def AddComment(self, message):
2517 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2518 msg=message)
2519
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002520 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002521 # DETAILED_ACCOUNTS is to get emails in accounts.
2522 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2523 summary = []
2524 for msg in data.get('messages', []):
2525 # Gerrit spits out nanoseconds.
2526 assert len(msg['date'].split('.')[-1]) == 9
2527 date = datetime.datetime.strptime(msg['date'][:-3],
2528 '%Y-%m-%d %H:%M:%S.%f')
2529 summary.append(_CommentSummary(
2530 date=date,
2531 message=msg['message'],
2532 sender=msg['author']['email'],
2533 # These could be inferred from the text messages and correlated with
2534 # Code-Review label maximum, however this is not reliable.
2535 # Leaving as is until the need arises.
2536 approval=False,
2537 disapproval=False,
2538 ))
2539 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002540
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002541 def CloseIssue(self):
2542 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2543
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002544 def GetApprovingReviewers(self):
2545 """Returns a list of reviewers approving the change.
2546
2547 Note: not necessarily committers.
2548 """
2549 raise NotImplementedError()
2550
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002551 def SubmitIssue(self, wait_for_merge=True):
2552 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2553 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002554
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002555 def _GetChangeDetail(self, options=None, issue=None,
2556 no_cache=False):
2557 """Returns details of the issue by querying Gerrit and caching results.
2558
2559 If fresh data is needed, set no_cache=True which will clear cache and
2560 thus new data will be fetched from Gerrit.
2561 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002562 options = options or []
2563 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002564 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002565
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002566 # Optimization to avoid multiple RPCs:
2567 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2568 'CURRENT_COMMIT' not in options):
2569 options.append('CURRENT_COMMIT')
2570
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002571 # Normalize issue and options for consistent keys in cache.
2572 issue = str(issue)
2573 options = [o.upper() for o in options]
2574
2575 # Check in cache first unless no_cache is True.
2576 if no_cache:
2577 self._detail_cache.pop(issue, None)
2578 else:
2579 options_set = frozenset(options)
2580 for cached_options_set, data in self._detail_cache.get(issue, []):
2581 # Assumption: data fetched before with extra options is suitable
2582 # for return for a smaller set of options.
2583 # For example, if we cached data for
2584 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2585 # and request is for options=[CURRENT_REVISION],
2586 # THEN we can return prior cached data.
2587 if options_set.issubset(cached_options_set):
2588 return data
2589
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002590 try:
2591 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2592 options, ignore_404=False)
2593 except gerrit_util.GerritError as e:
2594 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002595 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002596 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002597
2598 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002599 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002600
agable32978d92016-11-01 12:55:02 -07002601 def _GetChangeCommit(self, issue=None):
2602 issue = issue or self.GetIssue()
2603 assert issue, 'issue is required to query Gerrit'
2604 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2605 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002606 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002607 return data
2608
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002609 def CMDLand(self, force, bypass_hooks, verbose):
2610 if git_common.is_dirty_git_tree('land'):
2611 return 1
tandriid60367b2016-06-22 05:25:12 -07002612 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2613 if u'Commit-Queue' in detail.get('labels', {}):
2614 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002615 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2616 'which can test and land changes for you. '
2617 'Are you sure you wish to bypass it?\n',
2618 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002619
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002620 differs = True
tandriic4344b52016-08-29 06:04:54 -07002621 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002622 # Note: git diff outputs nothing if there is no diff.
2623 if not last_upload or RunGit(['diff', last_upload]).strip():
2624 print('WARNING: some changes from local branch haven\'t been uploaded')
2625 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002626 if detail['current_revision'] == last_upload:
2627 differs = False
2628 else:
2629 print('WARNING: local branch contents differ from latest uploaded '
2630 'patchset')
2631 if differs:
2632 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002633 confirm_or_exit(
2634 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2635 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002636 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2637 elif not bypass_hooks:
2638 hook_results = self.RunHook(
2639 committing=True,
2640 may_prompt=not force,
2641 verbose=verbose,
2642 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2643 if not hook_results.should_continue():
2644 return 1
2645
2646 self.SubmitIssue(wait_for_merge=True)
2647 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002648 links = self._GetChangeCommit().get('web_links', [])
2649 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002650 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002651 print('Landed as %s' % link.get('url'))
2652 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002653 return 0
2654
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002655 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2656 directory):
2657 assert not reject
2658 assert not nocommit
2659 assert not directory
2660 assert parsed_issue_arg.valid
2661
2662 self._changelist.issue = parsed_issue_arg.issue
2663
2664 if parsed_issue_arg.hostname:
2665 self._gerrit_host = parsed_issue_arg.hostname
2666 self._gerrit_server = 'https://%s' % self._gerrit_host
2667
tandriic2405f52016-10-10 08:13:15 -07002668 try:
2669 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002670 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002671 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002672
2673 if not parsed_issue_arg.patchset:
2674 # Use current revision by default.
2675 revision_info = detail['revisions'][detail['current_revision']]
2676 patchset = int(revision_info['_number'])
2677 else:
2678 patchset = parsed_issue_arg.patchset
2679 for revision_info in detail['revisions'].itervalues():
2680 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2681 break
2682 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002683 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002684 (parsed_issue_arg.patchset, self.GetIssue()))
2685
2686 fetch_info = revision_info['fetch']['http']
2687 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002688 self.SetIssue(self.GetIssue())
2689 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002690 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002691 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002692 (self.GetIssue(), self.GetPatchset()))
2693 return 0
2694
2695 @staticmethod
2696 def ParseIssueURL(parsed_url):
2697 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2698 return None
2699 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2700 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2701 # Short urls like https://domain/<issue_number> can be used, but don't allow
2702 # specifying the patchset (you'd 404), but we allow that here.
2703 if parsed_url.path == '/':
2704 part = parsed_url.fragment
2705 else:
2706 part = parsed_url.path
2707 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2708 if match:
2709 return _ParsedIssueNumberArgument(
2710 issue=int(match.group(2)),
2711 patchset=int(match.group(4)) if match.group(4) else None,
2712 hostname=parsed_url.netloc)
2713 return None
2714
tandrii16e0b4e2016-06-07 10:34:28 -07002715 def _GerritCommitMsgHookCheck(self, offer_removal):
2716 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2717 if not os.path.exists(hook):
2718 return
2719 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2720 # custom developer made one.
2721 data = gclient_utils.FileRead(hook)
2722 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2723 return
2724 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002725 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002726 'and may interfere with it in subtle ways.\n'
2727 'We recommend you remove the commit-msg hook.')
2728 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002729 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002730 gclient_utils.rm_file_or_tree(hook)
2731 print('Gerrit commit-msg hook removed.')
2732 else:
2733 print('OK, will keep Gerrit commit-msg hook in place.')
2734
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735 def CMDUploadChange(self, options, args, change):
2736 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002737 if options.squash and options.no_squash:
2738 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002739
2740 if not options.squash and not options.no_squash:
2741 # Load default for user, repo, squash=true, in this order.
2742 options.squash = settings.GetSquashGerritUploads()
2743 elif options.no_squash:
2744 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002745
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002746 # We assume the remote called "origin" is the one we want.
2747 # It is probably not worthwhile to support different workflows.
2748 gerrit_remote = 'origin'
2749
2750 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002751 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002752
Aaron Gableb56ad332017-01-06 15:24:31 -08002753 # This may be None; default fallback value is determined in logic below.
2754 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002755 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002756
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002758 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002759 if self.GetIssue():
2760 # Try to get the message from a previous upload.
2761 message = self.GetDescription()
2762 if not message:
2763 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002764 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002765 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002766 if not title:
2767 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2768 title = ask_for_data(
2769 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002770 if title == default_title:
2771 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002772 change_id = self._GetChangeDetail()['change_id']
2773 while True:
2774 footer_change_ids = git_footers.get_footer_change_id(message)
2775 if footer_change_ids == [change_id]:
2776 break
2777 if not footer_change_ids:
2778 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002779 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780 continue
2781 # There is already a valid footer but with different or several ids.
2782 # Doing this automatically is non-trivial as we don't want to lose
2783 # existing other footers, yet we want to append just 1 desired
2784 # Change-Id. Thus, just create a new footer, but let user verify the
2785 # new description.
2786 message = '%s\n\nChange-Id: %s' % (message, change_id)
2787 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002788 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002789 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002790 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002791 'Please, check the proposed correction to the description, '
2792 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2793 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2794 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002795 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002796 if not options.force:
2797 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002798 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002799 message = change_desc.description
2800 if not message:
2801 DieWithError("Description is empty. Aborting...")
2802 # Continue the while loop.
2803 # Sanity check of this code - we should end up with proper message
2804 # footer.
2805 assert [change_id] == git_footers.get_footer_change_id(message)
2806 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002807 else: # if not self.GetIssue()
2808 if options.message:
2809 message = options.message
2810 else:
2811 message = CreateDescriptionFromLog(args)
2812 if options.title:
2813 message = options.title + '\n\n' + message
2814 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002815 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002816 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002817 # On first upload, patchset title is always this string, while
2818 # --title flag gets converted to first line of message.
2819 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002820 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 if not change_desc.description:
2822 DieWithError("Description is empty. Aborting...")
2823 message = change_desc.description
2824 change_ids = git_footers.get_footer_change_id(message)
2825 if len(change_ids) > 1:
2826 DieWithError('too many Change-Id footers, at most 1 allowed.')
2827 if not change_ids:
2828 # Generate the Change-Id automatically.
2829 message = git_footers.add_footer_change_id(
2830 message, GenerateGerritChangeId(message))
2831 change_desc.set_description(message)
2832 change_ids = git_footers.get_footer_change_id(message)
2833 assert len(change_ids) == 1
2834 change_id = change_ids[0]
2835
2836 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2837 if remote is '.':
2838 # If our upstream branch is local, we base our squashed commit on its
2839 # squashed version.
2840 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2841 # Check the squashed hash of the parent.
2842 parent = RunGit(['config',
2843 'branch.%s.gerritsquashhash' % upstream_branch_name],
2844 error_ok=True).strip()
2845 # Verify that the upstream branch has been uploaded too, otherwise
2846 # Gerrit will create additional CLs when uploading.
2847 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2848 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002849 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002850 '\nUpload upstream branch %s first.\n'
2851 'It is likely that this branch has been rebased since its last '
2852 'upload, so you just need to upload it again.\n'
2853 '(If you uploaded it with --no-squash, then branch dependencies '
2854 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002855 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002856 else:
2857 parent = self.GetCommonAncestorWithUpstream()
2858
2859 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2860 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2861 '-m', message]).strip()
2862 else:
2863 change_desc = ChangeDescription(
2864 options.message or CreateDescriptionFromLog(args))
2865 if not change_desc.description:
2866 DieWithError("Description is empty. Aborting...")
2867
2868 if not git_footers.get_footer_change_id(change_desc.description):
2869 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002870 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2871 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002872 ref_to_push = 'HEAD'
2873 parent = '%s/%s' % (gerrit_remote, branch)
2874 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2875
2876 assert change_desc
2877 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2878 ref_to_push)]).splitlines()
2879 if len(commits) > 1:
2880 print('WARNING: This will upload %d commits. Run the following command '
2881 'to see which commits will be uploaded: ' % len(commits))
2882 print('git log %s..%s' % (parent, ref_to_push))
2883 print('You can also use `git squash-branch` to squash these into a '
2884 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002885 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886
2887 if options.reviewers or options.tbr_owners:
2888 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2889 change)
2890
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002891 # Extra options that can be specified at push time. Doc:
2892 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2893 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002894 if change_desc.get_reviewers(tbr_only=True):
2895 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2896 refspec_opts.append('l=Code-Review+1')
2897
Aaron Gable9b713dd2016-12-14 16:04:21 -08002898 if title:
2899 if not re.match(r'^[\w ]+$', title):
2900 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002901 if not automatic_title:
2902 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002903 'and spaces. You can edit it in the UI. '
2904 'See https://crbug.com/663787.\n'
2905 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002906 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2907 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002908 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002909
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002910 if options.send_mail:
2911 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002912 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002913 refspec_opts.append('notify=ALL')
2914 else:
2915 refspec_opts.append('notify=NONE')
2916
tandrii99a72f22016-08-17 14:33:24 -07002917 reviewers = change_desc.get_reviewers()
2918 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002919 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2920 # side for real (b/34702620).
2921 def clean_invisible_chars(email):
2922 return email.decode('unicode_escape').encode('ascii', 'ignore')
2923 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2924 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002925
agablec6787972016-09-09 16:13:34 -07002926 if options.private:
2927 refspec_opts.append('draft')
2928
rmistry9eadede2016-09-19 11:22:43 -07002929 if options.topic:
2930 # Documentation on Gerrit topics is here:
2931 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2932 refspec_opts.append('topic=%s' % options.topic)
2933
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002934 refspec_suffix = ''
2935 if refspec_opts:
2936 refspec_suffix = '%' + ','.join(refspec_opts)
2937 assert ' ' not in refspec_suffix, (
2938 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002939 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002940
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002941 try:
2942 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002943 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002944 print_stdout=True,
2945 # Flush after every line: useful for seeing progress when running as
2946 # recipe.
2947 filter_fn=lambda _: sys.stdout.flush())
2948 except subprocess2.CalledProcessError:
2949 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002950 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002951
2952 if options.squash:
2953 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2954 change_numbers = [m.group(1)
2955 for m in map(regex.match, push_stdout.splitlines())
2956 if m]
2957 if len(change_numbers) != 1:
2958 DieWithError(
2959 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002960 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002961 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002962 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002963
2964 # Add cc's from the CC_LIST and --cc flag (if any).
2965 cc = self.GetCCList().split(',')
2966 if options.cc:
2967 cc.extend(options.cc)
2968 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002969 if change_desc.get_cced():
2970 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002971 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002972 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002973 self._GetGerritHost(), self.GetIssue(), cc,
2974 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002975 return 0
2976
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002977 def _AddChangeIdToCommitMessage(self, options, args):
2978 """Re-commits using the current message, assumes the commit hook is in
2979 place.
2980 """
2981 log_desc = options.message or CreateDescriptionFromLog(args)
2982 git_command = ['commit', '--amend', '-m', log_desc]
2983 RunGit(git_command)
2984 new_log_desc = CreateDescriptionFromLog(args)
2985 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002986 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002987 return new_log_desc
2988 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002989 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002990
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002991 def SetCQState(self, new_state):
2992 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002993 vote_map = {
2994 _CQState.NONE: 0,
2995 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002996 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002997 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002998 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2999 if new_state == _CQState.DRY_RUN:
3000 # Don't spam everybody reviewer/owner.
3001 kwargs['notify'] = 'NONE'
3002 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003003
tandriie113dfd2016-10-11 10:20:12 -07003004 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003005 try:
3006 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003007 except GerritChangeNotExists:
3008 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003009
3010 if data['status'] in ('ABANDONED', 'MERGED'):
3011 return 'CL %s is closed' % self.GetIssue()
3012
3013 def GetTryjobProperties(self, patchset=None):
3014 """Returns dictionary of properties to launch tryjob."""
3015 data = self._GetChangeDetail(['ALL_REVISIONS'])
3016 patchset = int(patchset or self.GetPatchset())
3017 assert patchset
3018 revision_data = None # Pylint wants it to be defined.
3019 for revision_data in data['revisions'].itervalues():
3020 if int(revision_data['_number']) == patchset:
3021 break
3022 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003023 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003024 (patchset, self.GetIssue()))
3025 return {
3026 'patch_issue': self.GetIssue(),
3027 'patch_set': patchset or self.GetPatchset(),
3028 'patch_project': data['project'],
3029 'patch_storage': 'gerrit',
3030 'patch_ref': revision_data['fetch']['http']['ref'],
3031 'patch_repository_url': revision_data['fetch']['http']['url'],
3032 'patch_gerrit_url': self.GetCodereviewServer(),
3033 }
tandriie113dfd2016-10-11 10:20:12 -07003034
tandriide281ae2016-10-12 06:02:30 -07003035 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003036 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003037
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003038
3039_CODEREVIEW_IMPLEMENTATIONS = {
3040 'rietveld': _RietveldChangelistImpl,
3041 'gerrit': _GerritChangelistImpl,
3042}
3043
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003044
iannuccie53c9352016-08-17 14:40:40 -07003045def _add_codereview_issue_select_options(parser, extra=""):
3046 _add_codereview_select_options(parser)
3047
3048 text = ('Operate on this issue number instead of the current branch\'s '
3049 'implicit issue.')
3050 if extra:
3051 text += ' '+extra
3052 parser.add_option('-i', '--issue', type=int, help=text)
3053
3054
3055def _process_codereview_issue_select_options(parser, options):
3056 _process_codereview_select_options(parser, options)
3057 if options.issue is not None and not options.forced_codereview:
3058 parser.error('--issue must be specified with either --rietveld or --gerrit')
3059
3060
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003061def _add_codereview_select_options(parser):
3062 """Appends --gerrit and --rietveld options to force specific codereview."""
3063 parser.codereview_group = optparse.OptionGroup(
3064 parser, 'EXPERIMENTAL! Codereview override options')
3065 parser.add_option_group(parser.codereview_group)
3066 parser.codereview_group.add_option(
3067 '--gerrit', action='store_true',
3068 help='Force the use of Gerrit for codereview')
3069 parser.codereview_group.add_option(
3070 '--rietveld', action='store_true',
3071 help='Force the use of Rietveld for codereview')
3072
3073
3074def _process_codereview_select_options(parser, options):
3075 if options.gerrit and options.rietveld:
3076 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3077 options.forced_codereview = None
3078 if options.gerrit:
3079 options.forced_codereview = 'gerrit'
3080 elif options.rietveld:
3081 options.forced_codereview = 'rietveld'
3082
3083
tandriif9aefb72016-07-01 09:06:51 -07003084def _get_bug_line_values(default_project, bugs):
3085 """Given default_project and comma separated list of bugs, yields bug line
3086 values.
3087
3088 Each bug can be either:
3089 * a number, which is combined with default_project
3090 * string, which is left as is.
3091
3092 This function may produce more than one line, because bugdroid expects one
3093 project per line.
3094
3095 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3096 ['v8:123', 'chromium:789']
3097 """
3098 default_bugs = []
3099 others = []
3100 for bug in bugs.split(','):
3101 bug = bug.strip()
3102 if bug:
3103 try:
3104 default_bugs.append(int(bug))
3105 except ValueError:
3106 others.append(bug)
3107
3108 if default_bugs:
3109 default_bugs = ','.join(map(str, default_bugs))
3110 if default_project:
3111 yield '%s:%s' % (default_project, default_bugs)
3112 else:
3113 yield default_bugs
3114 for other in sorted(others):
3115 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3116 yield other
3117
3118
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003119class ChangeDescription(object):
3120 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003121 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003122 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003123 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003124 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003125
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003126 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003127 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003128
agable@chromium.org42c20792013-09-12 17:34:49 +00003129 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003130 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003131 return '\n'.join(self._description_lines)
3132
3133 def set_description(self, desc):
3134 if isinstance(desc, basestring):
3135 lines = desc.splitlines()
3136 else:
3137 lines = [line.rstrip() for line in desc]
3138 while lines and not lines[0]:
3139 lines.pop(0)
3140 while lines and not lines[-1]:
3141 lines.pop(-1)
3142 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143
piman@chromium.org336f9122014-09-04 02:16:55 +00003144 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003145 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003147 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003148 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003149 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003150
agable@chromium.org42c20792013-09-12 17:34:49 +00003151 # Get the set of R= and TBR= lines and remove them from the desciption.
3152 regexp = re.compile(self.R_LINE)
3153 matches = [regexp.match(line) for line in self._description_lines]
3154 new_desc = [l for i, l in enumerate(self._description_lines)
3155 if not matches[i]]
3156 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003157
agable@chromium.org42c20792013-09-12 17:34:49 +00003158 # Construct new unified R= and TBR= lines.
3159 r_names = []
3160 tbr_names = []
3161 for match in matches:
3162 if not match:
3163 continue
3164 people = cleanup_list([match.group(2).strip()])
3165 if match.group(1) == 'TBR':
3166 tbr_names.extend(people)
3167 else:
3168 r_names.extend(people)
3169 for name in r_names:
3170 if name not in reviewers:
3171 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003172 if add_owners_tbr:
3173 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003174 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003175 all_reviewers = set(tbr_names + reviewers)
3176 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3177 all_reviewers)
3178 tbr_names.extend(owners_db.reviewers_for(missing_files,
3179 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003180 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3181 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3182
3183 # Put the new lines in the description where the old first R= line was.
3184 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3185 if 0 <= line_loc < len(self._description_lines):
3186 if new_tbr_line:
3187 self._description_lines.insert(line_loc, new_tbr_line)
3188 if new_r_line:
3189 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003190 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003191 if new_r_line:
3192 self.append_footer(new_r_line)
3193 if new_tbr_line:
3194 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003195
Aaron Gable3a16ed12017-03-23 10:51:55 -07003196 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003197 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003198 self.set_description([
3199 '# Enter a description of the change.',
3200 '# This will be displayed on the codereview site.',
3201 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003202 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003203 '--------------------',
3204 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003205
agable@chromium.org42c20792013-09-12 17:34:49 +00003206 regexp = re.compile(self.BUG_LINE)
3207 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003208 prefix = settings.GetBugPrefix()
3209 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003210 if git_footer:
3211 self.append_footer('Bug: %s' % ', '.join(values))
3212 else:
3213 for value in values:
3214 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003215
agable@chromium.org42c20792013-09-12 17:34:49 +00003216 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003217 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003218 if not content:
3219 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003220 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003221
3222 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003223 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3224 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003225 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003226 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003227
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003228 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003229 """Adds a footer line to the description.
3230
3231 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3232 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3233 that Gerrit footers are always at the end.
3234 """
3235 parsed_footer_line = git_footers.parse_footer(line)
3236 if parsed_footer_line:
3237 # Line is a gerrit footer in the form: Footer-Key: any value.
3238 # Thus, must be appended observing Gerrit footer rules.
3239 self.set_description(
3240 git_footers.add_footer(self.description,
3241 key=parsed_footer_line[0],
3242 value=parsed_footer_line[1]))
3243 return
3244
3245 if not self._description_lines:
3246 self._description_lines.append(line)
3247 return
3248
3249 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3250 if gerrit_footers:
3251 # git_footers.split_footers ensures that there is an empty line before
3252 # actual (gerrit) footers, if any. We have to keep it that way.
3253 assert top_lines and top_lines[-1] == ''
3254 top_lines, separator = top_lines[:-1], top_lines[-1:]
3255 else:
3256 separator = [] # No need for separator if there are no gerrit_footers.
3257
3258 prev_line = top_lines[-1] if top_lines else ''
3259 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3260 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3261 top_lines.append('')
3262 top_lines.append(line)
3263 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003264
tandrii99a72f22016-08-17 14:33:24 -07003265 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003266 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003267 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003268 reviewers = [match.group(2).strip()
3269 for match in matches
3270 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003271 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003272
bradnelsond975b302016-10-23 12:20:23 -07003273 def get_cced(self):
3274 """Retrieves the list of reviewers."""
3275 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3276 cced = [match.group(2).strip() for match in matches if match]
3277 return cleanup_list(cced)
3278
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003279 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3280 """Updates this commit description given the parent.
3281
3282 This is essentially what Gnumbd used to do.
3283 Consult https://goo.gl/WMmpDe for more details.
3284 """
3285 assert parent_msg # No, orphan branch creation isn't supported.
3286 assert parent_hash
3287 assert dest_ref
3288 parent_footer_map = git_footers.parse_footers(parent_msg)
3289 # This will also happily parse svn-position, which GnumbD is no longer
3290 # supporting. While we'd generate correct footers, the verifier plugin
3291 # installed in Gerrit will block such commit (ie git push below will fail).
3292 parent_position = git_footers.get_position(parent_footer_map)
3293
3294 # Cherry-picks may have last line obscuring their prior footers,
3295 # from git_footers perspective. This is also what Gnumbd did.
3296 cp_line = None
3297 if (self._description_lines and
3298 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3299 cp_line = self._description_lines.pop()
3300
3301 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3302
3303 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3304 # user interference with actual footers we'd insert below.
3305 for i, (k, v) in enumerate(parsed_footers):
3306 if k.startswith('Cr-'):
3307 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3308
3309 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003310 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003311 if parent_position[0] == dest_ref:
3312 # Same branch as parent.
3313 number = int(parent_position[1]) + 1
3314 else:
3315 number = 1 # New branch, and extra lineage.
3316 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3317 int(parent_position[1])))
3318
3319 parsed_footers.append(('Cr-Commit-Position',
3320 '%s@{#%d}' % (dest_ref, number)))
3321 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3322
3323 self._description_lines = top_lines
3324 if cp_line:
3325 self._description_lines.append(cp_line)
3326 if self._description_lines[-1] != '':
3327 self._description_lines.append('') # Ensure footer separator.
3328 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3329
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003330
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003331def get_approving_reviewers(props):
3332 """Retrieves the reviewers that approved a CL from the issue properties with
3333 messages.
3334
3335 Note that the list may contain reviewers that are not committer, thus are not
3336 considered by the CQ.
3337 """
3338 return sorted(
3339 set(
3340 message['sender']
3341 for message in props['messages']
3342 if message['approval'] and message['sender'] in props['reviewers']
3343 )
3344 )
3345
3346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003347def FindCodereviewSettingsFile(filename='codereview.settings'):
3348 """Finds the given file starting in the cwd and going up.
3349
3350 Only looks up to the top of the repository unless an
3351 'inherit-review-settings-ok' file exists in the root of the repository.
3352 """
3353 inherit_ok_file = 'inherit-review-settings-ok'
3354 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003355 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3357 root = '/'
3358 while True:
3359 if filename in os.listdir(cwd):
3360 if os.path.isfile(os.path.join(cwd, filename)):
3361 return open(os.path.join(cwd, filename))
3362 if cwd == root:
3363 break
3364 cwd = os.path.dirname(cwd)
3365
3366
3367def LoadCodereviewSettingsFromFile(fileobj):
3368 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003369 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371 def SetProperty(name, setting, unset_error_ok=False):
3372 fullname = 'rietveld.' + name
3373 if setting in keyvals:
3374 RunGit(['config', fullname, keyvals[setting]])
3375 else:
3376 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3377
tandrii48df5812016-10-17 03:55:37 -07003378 if not keyvals.get('GERRIT_HOST', False):
3379 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003380 # Only server setting is required. Other settings can be absent.
3381 # In that case, we ignore errors raised during option deletion attempt.
3382 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003383 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3385 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003386 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003387 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3388 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003389 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003390 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3391 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003393 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003394 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003395
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003396 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003397 RunGit(['config', 'gerrit.squash-uploads',
3398 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003399
tandrii@chromium.org28253532016-04-14 13:46:56 +00003400 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003401 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003402 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003404 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003405 # should be of the form
3406 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3407 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3409 keyvals['ORIGIN_URL_CONFIG']])
3410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003411
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003412def urlretrieve(source, destination):
3413 """urllib is broken for SSL connections via a proxy therefore we
3414 can't use urllib.urlretrieve()."""
3415 with open(destination, 'w') as f:
3416 f.write(urllib2.urlopen(source).read())
3417
3418
ukai@chromium.org712d6102013-11-27 00:52:58 +00003419def hasSheBang(fname):
3420 """Checks fname is a #! script."""
3421 with open(fname) as f:
3422 return f.read(2).startswith('#!')
3423
3424
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003425# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3426def DownloadHooks(*args, **kwargs):
3427 pass
3428
3429
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003430def DownloadGerritHook(force):
3431 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003432
3433 Args:
3434 force: True to update hooks. False to install hooks if not present.
3435 """
3436 if not settings.GetIsGerrit():
3437 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003438 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003439 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3440 if not os.access(dst, os.X_OK):
3441 if os.path.exists(dst):
3442 if not force:
3443 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003444 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003445 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003446 if not hasSheBang(dst):
3447 DieWithError('Not a script: %s\n'
3448 'You need to download from\n%s\n'
3449 'into .git/hooks/commit-msg and '
3450 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003451 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3452 except Exception:
3453 if os.path.exists(dst):
3454 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003455 DieWithError('\nFailed to download hooks.\n'
3456 'You need to download from\n%s\n'
3457 'into .git/hooks/commit-msg and '
3458 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003459
3460
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003461def GetRietveldCodereviewSettingsInteractively():
3462 """Prompt the user for settings."""
3463 server = settings.GetDefaultServerUrl(error_ok=True)
3464 prompt = 'Rietveld server (host[:port])'
3465 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3466 newserver = ask_for_data(prompt + ':')
3467 if not server and not newserver:
3468 newserver = DEFAULT_SERVER
3469 if newserver:
3470 newserver = gclient_utils.UpgradeToHttps(newserver)
3471 if newserver != server:
3472 RunGit(['config', 'rietveld.server', newserver])
3473
3474 def SetProperty(initial, caption, name, is_url):
3475 prompt = caption
3476 if initial:
3477 prompt += ' ("x" to clear) [%s]' % initial
3478 new_val = ask_for_data(prompt + ':')
3479 if new_val == 'x':
3480 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3481 elif new_val:
3482 if is_url:
3483 new_val = gclient_utils.UpgradeToHttps(new_val)
3484 if new_val != initial:
3485 RunGit(['config', 'rietveld.' + name, new_val])
3486
3487 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3488 SetProperty(settings.GetDefaultPrivateFlag(),
3489 'Private flag (rietveld only)', 'private', False)
3490 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3491 'tree-status-url', False)
3492 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3493 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3494 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3495 'run-post-upload-hook', False)
3496
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003497
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003498class _GitCookiesChecker(object):
3499 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003500
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003501 _GOOGLESOURCE = 'googlesource.com'
3502
3503 def __init__(self):
3504 # Cached list of [host, identity, source], where source is either
3505 # .gitcookies or .netrc.
3506 self._all_hosts = None
3507
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003508 def ensure_configured_gitcookies(self):
3509 """Runs checks and suggests fixes to make git use .gitcookies from default
3510 path."""
3511 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3512 configured_path = RunGitSilent(
3513 ['config', '--global', 'http.cookiefile']).strip()
3514 if configured_path:
3515 self._ensure_default_gitcookies_path(configured_path, default)
3516 else:
3517 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003518
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003519 @staticmethod
3520 def _ensure_default_gitcookies_path(configured_path, default_path):
3521 assert configured_path
3522 if configured_path == default_path:
3523 print('git is already configured to use your .gitcookies from %s' %
3524 configured_path)
3525 return
3526
3527 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3528 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3529 (configured_path, default_path))
3530
3531 if not os.path.exists(configured_path):
3532 print('However, your configured .gitcookies file is missing.')
3533 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3534 action='reconfigure')
3535 RunGit(['config', '--global', 'http.cookiefile', default_path])
3536 return
3537
3538 if os.path.exists(default_path):
3539 print('WARNING: default .gitcookies file already exists %s' %
3540 default_path)
3541 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3542 default_path)
3543
3544 confirm_or_exit('Move existing .gitcookies to default location?',
3545 action='move')
3546 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003547 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003548 print('Moved and reconfigured git to use .gitcookies from %s' %
3549 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003550
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003551 @staticmethod
3552 def _configure_gitcookies_path(default_path):
3553 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3554 if os.path.exists(netrc_path):
3555 print('You seem to be using outdated .netrc for git credentials: %s' %
3556 netrc_path)
3557 print('This tool will guide you through setting up recommended '
3558 '.gitcookies store for git credentials.\n'
3559 '\n'
3560 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3561 ' git config --global --unset http.cookiefile\n'
3562 ' mv %s %s.backup\n\n' % (default_path, default_path))
3563 confirm_or_exit(action='setup .gitcookies')
3564 RunGit(['config', '--global', 'http.cookiefile', default_path])
3565 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003566
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003567 def get_hosts_with_creds(self, include_netrc=False):
3568 if self._all_hosts is None:
3569 a = gerrit_util.CookiesAuthenticator()
3570 self._all_hosts = [
3571 (h, u, s)
3572 for h, u, s in itertools.chain(
3573 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3574 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3575 )
3576 if h.endswith(self._GOOGLESOURCE)
3577 ]
3578
3579 if include_netrc:
3580 return self._all_hosts
3581 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3582
3583 def print_current_creds(self, include_netrc=False):
3584 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3585 if not hosts:
3586 print('No Git/Gerrit credentials found')
3587 return
3588 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3589 header = [('Host', 'User', 'Which file'),
3590 ['=' * l for l in lengths]]
3591 for row in (header + hosts):
3592 print('\t'.join((('%%+%ds' % l) % s)
3593 for l, s in zip(lengths, row)))
3594
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003595 @staticmethod
3596 def _parse_identity(identity):
3597 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3598 username, domain = identity.split('.', 1)
3599 if username.startswith('git-'):
3600 username = username[len('git-'):]
3601 return username, domain
3602
3603 def _get_usernames_of_domain(self, domain):
3604 """Returns list of usernames referenced by .gitcookies in a given domain."""
3605 identities_by_domain = {}
3606 for _, identity, _ in self.get_hosts_with_creds():
3607 username, domain = self._parse_identity(identity)
3608 identities_by_domain.setdefault(domain, []).append(username)
3609 return identities_by_domain.get(domain)
3610
3611 def _canonical_git_googlesource_host(self, host):
3612 """Normalizes Gerrit hosts (with '-review') to Git host."""
3613 assert host.endswith(self._GOOGLESOURCE)
3614 # Prefix doesn't include '.' at the end.
3615 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3616 if prefix.endswith('-review'):
3617 prefix = prefix[:-len('-review')]
3618 return prefix + '.' + self._GOOGLESOURCE
3619
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003620 def _canonical_gerrit_googlesource_host(self, host):
3621 git_host = self._canonical_git_googlesource_host(host)
3622 prefix = git_host.split('.', 1)[0]
3623 return prefix + '-review.' + self._GOOGLESOURCE
3624
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003625 def has_generic_host(self):
3626 """Returns whether generic .googlesource.com has been configured.
3627
3628 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3629 """
3630 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3631 if host == '.' + self._GOOGLESOURCE:
3632 return True
3633 return False
3634
3635 def _get_git_gerrit_identity_pairs(self):
3636 """Returns map from canonic host to pair of identities (Git, Gerrit).
3637
3638 One of identities might be None, meaning not configured.
3639 """
3640 host_to_identity_pairs = {}
3641 for host, identity, _ in self.get_hosts_with_creds():
3642 canonical = self._canonical_git_googlesource_host(host)
3643 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3644 idx = 0 if canonical == host else 1
3645 pair[idx] = identity
3646 return host_to_identity_pairs
3647
3648 def get_partially_configured_hosts(self):
3649 return set(
3650 host for host, identities_pair in
3651 self._get_git_gerrit_identity_pairs().iteritems()
3652 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3653
3654 def get_conflicting_hosts(self):
3655 return set(
3656 host for host, (i1, i2) in
3657 self._get_git_gerrit_identity_pairs().iteritems()
3658 if None not in (i1, i2) and i1 != i2)
3659
3660 def get_duplicated_hosts(self):
3661 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3662 return set(host for host, count in counters.iteritems() if count > 1)
3663
3664 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3665 'chromium.googlesource.com': 'chromium.org',
3666 'chrome-internal.googlesource.com': 'google.com',
3667 }
3668
3669 def get_hosts_with_wrong_identities(self):
3670 """Finds hosts which **likely** reference wrong identities.
3671
3672 Note: skips hosts which have conflicting identities for Git and Gerrit.
3673 """
3674 hosts = set()
3675 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3676 pair = self._get_git_gerrit_identity_pairs().get(host)
3677 if pair and pair[0] == pair[1]:
3678 _, domain = self._parse_identity(pair[0])
3679 if domain != expected:
3680 hosts.add(host)
3681 return hosts
3682
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003683 @staticmethod
3684 def print_hosts(hosts, extra_column_func=None):
3685 hosts = sorted(hosts)
3686 assert hosts
3687 if extra_column_func is None:
3688 extras = [''] * len(hosts)
3689 else:
3690 extras = [extra_column_func(host) for host in hosts]
3691 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3692 for he in zip(hosts, extras):
3693 print(tmpl % he)
3694 print()
3695
3696 def find_and_report_problems(self):
3697 """Returns True if there was at least one problem, else False."""
3698 problems = [False]
3699 def add_problem():
3700 if not problems[0]:
3701 print('.gitcookies problem report:\n')
3702 problems[0] = True
3703
3704 if self.has_generic_host():
3705 add_problem()
3706 print(' .googlesource.com record detected\n'
3707 ' Chrome Infrastructure team recommends to list full host names '
3708 'explicitly.\n')
3709
3710 dups = self.get_duplicated_hosts()
3711 if dups:
3712 add_problem()
3713 print(' The following hosts were defined twice:\n')
3714 self.print_hosts(dups)
3715
3716 partial = self.get_partially_configured_hosts()
3717 if partial:
3718 add_problem()
3719 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3720 'These hosts are missing:')
3721 self.print_hosts(partial)
3722
3723 conflicting = self.get_conflicting_hosts()
3724 if conflicting:
3725 add_problem()
3726 print(' The following Git hosts have differing credentials from their '
3727 'Gerrit counterparts:\n')
3728 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3729 tuple(self._get_git_gerrit_identity_pairs()[host]))
3730
3731 wrong = self.get_hosts_with_wrong_identities()
3732 if wrong:
3733 add_problem()
3734 print(' These hosts likely use wrong identity:\n')
3735 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3736 (self._get_git_gerrit_identity_pairs()[host][0],
3737 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3738 return problems[0]
3739
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003740
3741def CMDcreds_check(parser, args):
3742 """Checks credentials and suggests changes."""
3743 _, _ = parser.parse_args(args)
3744
3745 if gerrit_util.GceAuthenticator.is_gce():
3746 DieWithError('this command is not designed for GCE, are you on a bot?')
3747
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003748 checker = _GitCookiesChecker()
3749 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003750
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003751 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003752 checker.print_current_creds(include_netrc=True)
3753
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003754 if not checker.find_and_report_problems():
3755 print('\nNo problems detected in your .gitcookies')
3756 return 0
3757 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003758
3759
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003760@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003762 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763
tandrii5d0a0422016-09-14 06:24:35 -07003764 print('WARNING: git cl config works for Rietveld only')
3765 # TODO(tandrii): remove this once we switch to Gerrit.
3766 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003767 parser.add_option('--activate-update', action='store_true',
3768 help='activate auto-updating [rietveld] section in '
3769 '.git/config')
3770 parser.add_option('--deactivate-update', action='store_true',
3771 help='deactivate auto-updating [rietveld] section in '
3772 '.git/config')
3773 options, args = parser.parse_args(args)
3774
3775 if options.deactivate_update:
3776 RunGit(['config', 'rietveld.autoupdate', 'false'])
3777 return
3778
3779 if options.activate_update:
3780 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3781 return
3782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003783 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003784 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785 return 0
3786
3787 url = args[0]
3788 if not url.endswith('codereview.settings'):
3789 url = os.path.join(url, 'codereview.settings')
3790
3791 # Load code review settings and download hooks (if available).
3792 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3793 return 0
3794
3795
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003796def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003797 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003798 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3799 branch = ShortBranchName(branchref)
3800 _, args = parser.parse_args(args)
3801 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003803 return RunGit(['config', 'branch.%s.base-url' % branch],
3804 error_ok=False).strip()
3805 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003806 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003807 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3808 error_ok=False).strip()
3809
3810
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003811def color_for_status(status):
3812 """Maps a Changelist status to color, for CMDstatus and other tools."""
3813 return {
3814 'unsent': Fore.RED,
3815 'waiting': Fore.BLUE,
3816 'reply': Fore.YELLOW,
3817 'lgtm': Fore.GREEN,
3818 'commit': Fore.MAGENTA,
3819 'closed': Fore.CYAN,
3820 'error': Fore.WHITE,
3821 }.get(status, Fore.WHITE)
3822
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003823
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003824def get_cl_statuses(changes, fine_grained, max_processes=None):
3825 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003826
3827 If fine_grained is true, this will fetch CL statuses from the server.
3828 Otherwise, simply indicate if there's a matching url for the given branches.
3829
3830 If max_processes is specified, it is used as the maximum number of processes
3831 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3832 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003833
3834 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003835 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003836 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003837 upload.verbosity = 0
3838
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003839 if not changes:
3840 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003841
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003842 if not fine_grained:
3843 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003844 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003845 for cl in changes:
3846 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003847 return
3848
3849 # First, sort out authentication issues.
3850 logging.debug('ensuring credentials exist')
3851 for cl in changes:
3852 cl.EnsureAuthenticated(force=False, refresh=True)
3853
3854 def fetch(cl):
3855 try:
3856 return (cl, cl.GetStatus())
3857 except:
3858 # See http://crbug.com/629863.
3859 logging.exception('failed to fetch status for %s:', cl)
3860 raise
3861
3862 threads_count = len(changes)
3863 if max_processes:
3864 threads_count = max(1, min(threads_count, max_processes))
3865 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3866
3867 pool = ThreadPool(threads_count)
3868 fetched_cls = set()
3869 try:
3870 it = pool.imap_unordered(fetch, changes).__iter__()
3871 while True:
3872 try:
3873 cl, status = it.next(timeout=5)
3874 except multiprocessing.TimeoutError:
3875 break
3876 fetched_cls.add(cl)
3877 yield cl, status
3878 finally:
3879 pool.close()
3880
3881 # Add any branches that failed to fetch.
3882 for cl in set(changes) - fetched_cls:
3883 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003884
rmistry@google.com2dd99862015-06-22 12:22:18 +00003885
3886def upload_branch_deps(cl, args):
3887 """Uploads CLs of local branches that are dependents of the current branch.
3888
3889 If the local branch dependency tree looks like:
3890 test1 -> test2.1 -> test3.1
3891 -> test3.2
3892 -> test2.2 -> test3.3
3893
3894 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3895 run on the dependent branches in this order:
3896 test2.1, test3.1, test3.2, test2.2, test3.3
3897
3898 Note: This function does not rebase your local dependent branches. Use it when
3899 you make a change to the parent branch that will not conflict with its
3900 dependent branches, and you would like their dependencies updated in
3901 Rietveld.
3902 """
3903 if git_common.is_dirty_git_tree('upload-branch-deps'):
3904 return 1
3905
3906 root_branch = cl.GetBranch()
3907 if root_branch is None:
3908 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3909 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003910 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003911 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3912 'patchset dependencies without an uploaded CL.')
3913
3914 branches = RunGit(['for-each-ref',
3915 '--format=%(refname:short) %(upstream:short)',
3916 'refs/heads'])
3917 if not branches:
3918 print('No local branches found.')
3919 return 0
3920
3921 # Create a dictionary of all local branches to the branches that are dependent
3922 # on it.
3923 tracked_to_dependents = collections.defaultdict(list)
3924 for b in branches.splitlines():
3925 tokens = b.split()
3926 if len(tokens) == 2:
3927 branch_name, tracked = tokens
3928 tracked_to_dependents[tracked].append(branch_name)
3929
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print()
3931 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003932 dependents = []
3933 def traverse_dependents_preorder(branch, padding=''):
3934 dependents_to_process = tracked_to_dependents.get(branch, [])
3935 padding += ' '
3936 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003937 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003938 dependents.append(dependent)
3939 traverse_dependents_preorder(dependent, padding)
3940 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003941 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003942
3943 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003944 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003945 return 0
3946
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003947 confirm_or_exit('This command will checkout all dependent branches and run '
3948 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003949
andybons@chromium.org962f9462016-02-03 20:00:42 +00003950 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003951 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003952 args.extend(['-t', 'Updated patchset dependency'])
3953
rmistry@google.com2dd99862015-06-22 12:22:18 +00003954 # Record all dependents that failed to upload.
3955 failures = {}
3956 # Go through all dependents, checkout the branch and upload.
3957 try:
3958 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003959 print()
3960 print('--------------------------------------')
3961 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003962 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003963 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003964 try:
3965 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003966 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003967 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003968 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003969 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003970 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003971 finally:
3972 # Swap back to the original root branch.
3973 RunGit(['checkout', '-q', root_branch])
3974
vapiera7fbd5a2016-06-16 09:17:49 -07003975 print()
3976 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003977 for dependent_branch in dependents:
3978 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003979 print(' %s : %s' % (dependent_branch, upload_status))
3980 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003981
3982 return 0
3983
3984
kmarshall3bff56b2016-06-06 18:31:47 -07003985def CMDarchive(parser, args):
3986 """Archives and deletes branches associated with closed changelists."""
3987 parser.add_option(
3988 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003989 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003990 parser.add_option(
3991 '-f', '--force', action='store_true',
3992 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003993 parser.add_option(
3994 '-d', '--dry-run', action='store_true',
3995 help='Skip the branch tagging and removal steps.')
3996 parser.add_option(
3997 '-t', '--notags', action='store_true',
3998 help='Do not tag archived branches. '
3999 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004000
4001 auth.add_auth_options(parser)
4002 options, args = parser.parse_args(args)
4003 if args:
4004 parser.error('Unsupported args: %s' % ' '.join(args))
4005 auth_config = auth.extract_auth_config_from_options(options)
4006
4007 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4008 if not branches:
4009 return 0
4010
vapiera7fbd5a2016-06-16 09:17:49 -07004011 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004012 changes = [Changelist(branchref=b, auth_config=auth_config)
4013 for b in branches.splitlines()]
4014 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4015 statuses = get_cl_statuses(changes,
4016 fine_grained=True,
4017 max_processes=options.maxjobs)
4018 proposal = [(cl.GetBranch(),
4019 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4020 for cl, status in statuses
4021 if status == 'closed']
4022 proposal.sort()
4023
4024 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004025 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004026 return 0
4027
4028 current_branch = GetCurrentBranch()
4029
vapiera7fbd5a2016-06-16 09:17:49 -07004030 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004031 if options.notags:
4032 for next_item in proposal:
4033 print(' ' + next_item[0])
4034 else:
4035 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4036 for next_item in proposal:
4037 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004038
kmarshall9249e012016-08-23 12:02:16 -07004039 # Quit now on precondition failure or if instructed by the user, either
4040 # via an interactive prompt or by command line flags.
4041 if options.dry_run:
4042 print('\nNo changes were made (dry run).\n')
4043 return 0
4044 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004045 print('You are currently on a branch \'%s\' which is associated with a '
4046 'closed codereview issue, so archive cannot proceed. Please '
4047 'checkout another branch and run this command again.' %
4048 current_branch)
4049 return 1
kmarshall9249e012016-08-23 12:02:16 -07004050 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004051 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4052 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004054 return 1
4055
4056 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004057 if not options.notags:
4058 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004059 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004060
vapiera7fbd5a2016-06-16 09:17:49 -07004061 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004062
4063 return 0
4064
4065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004067 """Show status of changelists.
4068
4069 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004070 - Red not sent for review or broken
4071 - Blue waiting for review
4072 - Yellow waiting for you to reply to review
4073 - Green LGTM'ed
4074 - Magenta in the commit queue
4075 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004076
4077 Also see 'git cl comments'.
4078 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004079 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004080 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004081 parser.add_option('-f', '--fast', action='store_true',
4082 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004083 parser.add_option(
4084 '-j', '--maxjobs', action='store', type=int,
4085 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004086
4087 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004088 _add_codereview_issue_select_options(
4089 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004090 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004091 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004092 if args:
4093 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004094 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095
iannuccie53c9352016-08-17 14:40:40 -07004096 if options.issue is not None and not options.field:
4097 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004099 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004100 cl = Changelist(auth_config=auth_config, issue=options.issue,
4101 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004103 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004104 elif options.field == 'id':
4105 issueid = cl.GetIssue()
4106 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004107 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108 elif options.field == 'patch':
4109 patchset = cl.GetPatchset()
4110 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004112 elif options.field == 'status':
4113 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 elif options.field == 'url':
4115 url = cl.GetIssueURL()
4116 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004117 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004118 return 0
4119
4120 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4121 if not branches:
4122 print('No local branch found.')
4123 return 0
4124
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004125 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004126 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004127 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004128 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004129 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004130 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004131 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004132
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004133 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004134 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4135 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4136 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004137 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004138 c, status = output.next()
4139 branch_statuses[c.GetBranch()] = status
4140 status = branch_statuses.pop(branch)
4141 url = cl.GetIssueURL()
4142 if url and (not status or status == 'error'):
4143 # The issue probably doesn't exist anymore.
4144 url += ' (broken)'
4145
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004146 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004147 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004148 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004149 color = ''
4150 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004151 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004152 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004153 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004154 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004155
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004156
4157 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004158 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004159 print('Current branch: %s' % branch)
4160 for cl in changes:
4161 if cl.GetBranch() == branch:
4162 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004163 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004164 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004165 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004166 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004167 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print('Issue description:')
4169 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004170 return 0
4171
4172
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004173def colorize_CMDstatus_doc():
4174 """To be called once in main() to add colors to git cl status help."""
4175 colors = [i for i in dir(Fore) if i[0].isupper()]
4176
4177 def colorize_line(line):
4178 for color in colors:
4179 if color in line.upper():
4180 # Extract whitespaces first and the leading '-'.
4181 indent = len(line) - len(line.lstrip(' ')) + 1
4182 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4183 return line
4184
4185 lines = CMDstatus.__doc__.splitlines()
4186 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4187
4188
phajdan.jre328cf92016-08-22 04:12:17 -07004189def write_json(path, contents):
4190 with open(path, 'w') as f:
4191 json.dump(contents, f)
4192
4193
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004194@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004196 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004197
4198 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004199 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004200 parser.add_option('-r', '--reverse', action='store_true',
4201 help='Lookup the branch(es) for the specified issues. If '
4202 'no issues are specified, all branches with mapped '
4203 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004204 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004205 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004206 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004207 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208
dnj@chromium.org406c4402015-03-03 17:22:28 +00004209 if options.reverse:
4210 branches = RunGit(['for-each-ref', 'refs/heads',
4211 '--format=%(refname:short)']).splitlines()
4212
4213 # Reverse issue lookup.
4214 issue_branch_map = {}
4215 for branch in branches:
4216 cl = Changelist(branchref=branch)
4217 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4218 if not args:
4219 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004220 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004221 for issue in args:
4222 if not issue:
4223 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004224 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004225 print('Branch for issue number %s: %s' % (
4226 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004227 if options.json:
4228 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004229 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004230 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004231 if len(args) > 0:
4232 try:
4233 issue = int(args[0])
4234 except ValueError:
4235 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004236 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004237 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004238 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004239 if options.json:
4240 write_json(options.json, {
4241 'issue': cl.GetIssue(),
4242 'issue_url': cl.GetIssueURL(),
4243 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004244 return 0
4245
4246
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004247def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004248 """Shows or posts review comments for any changelist."""
4249 parser.add_option('-a', '--add-comment', dest='comment',
4250 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004251 parser.add_option('-i', '--issue', dest='issue',
4252 help='review issue id (defaults to current issue). '
4253 'If given, requires --rietveld or --gerrit')
smut@google.comc85ac942015-09-15 16:34:43 +00004254 parser.add_option('-j', '--json-file',
4255 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004256 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004257 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004258 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004259 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004260 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004261
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004262 issue = None
4263 if options.issue:
4264 try:
4265 issue = int(options.issue)
4266 except ValueError:
4267 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004268 if not options.forced_codereview:
4269 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004270
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004271 cl = Changelist(issue=issue,
4272 # TODO(tandrii): remove 'rietveld' default.
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01004273 codereview=options.forced_codereview or (
4274 'rietveld' if issue else None),
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004275 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004276
4277 if options.comment:
4278 cl.AddComment(options.comment)
4279 return 0
4280
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004281 summary = sorted(cl.GetCommentsSummary(), key=lambda c: c.date)
4282 for comment in summary:
4283 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004284 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004285 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004286 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004287 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004288 color = Fore.MAGENTA
4289 else:
4290 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004291 print('\n%s%s %s%s\n%s' % (
4292 color,
4293 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4294 comment.sender,
4295 Fore.RESET,
4296 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4297
smut@google.comc85ac942015-09-15 16:34:43 +00004298 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004299 def pre_serialize(c):
4300 dct = c.__dict__.copy()
4301 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4302 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004303 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004304 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004305 return 0
4306
4307
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004308@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004309def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004310 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004311 parser.add_option('-d', '--display', action='store_true',
4312 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004313 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004314 help='New description to set for this issue (- for stdin, '
4315 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004316 parser.add_option('-f', '--force', action='store_true',
4317 help='Delete any unpublished Gerrit edits for this issue '
4318 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004319
4320 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004321 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004322 options, args = parser.parse_args(args)
4323 _process_codereview_select_options(parser, options)
4324
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004325 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004326 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004327 target_issue_arg = ParseIssueNumberArgument(args[0])
4328 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004329 parser.print_help()
4330 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004331
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004332 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004333
martiniss6eda05f2016-06-30 10:18:35 -07004334 kwargs = {
4335 'auth_config': auth_config,
4336 'codereview': options.forced_codereview,
4337 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004338 if target_issue_arg:
4339 kwargs['issue'] = target_issue_arg.issue
4340 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004341
4342 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004343
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004344 if not cl.GetIssue():
4345 DieWithError('This branch has no associated changelist.')
4346 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004347
smut@google.com34fb6b12015-07-13 20:03:26 +00004348 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004349 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004350 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004351
4352 if options.new_description:
4353 text = options.new_description
4354 if text == '-':
4355 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004356 elif text == '+':
4357 base_branch = cl.GetCommonAncestorWithUpstream()
4358 change = cl.GetChange(base_branch, None, local_description=True)
4359 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004360
4361 description.set_description(text)
4362 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004363 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004364
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004365 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004366 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004367 return 0
4368
4369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004370def CreateDescriptionFromLog(args):
4371 """Pulls out the commit log to use as a base for the CL description."""
4372 log_args = []
4373 if len(args) == 1 and not args[0].endswith('.'):
4374 log_args = [args[0] + '..']
4375 elif len(args) == 1 and args[0].endswith('...'):
4376 log_args = [args[0][:-1]]
4377 elif len(args) == 2:
4378 log_args = [args[0] + '..' + args[1]]
4379 else:
4380 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004381 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382
4383
thestig@chromium.org44202a22014-03-11 19:22:18 +00004384def CMDlint(parser, args):
4385 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004386 parser.add_option('--filter', action='append', metavar='-x,+y',
4387 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004388 auth.add_auth_options(parser)
4389 options, args = parser.parse_args(args)
4390 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004391
4392 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004393 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004394 try:
4395 import cpplint
4396 import cpplint_chromium
4397 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004398 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004399 return 1
4400
4401 # Change the current working directory before calling lint so that it
4402 # shows the correct base.
4403 previous_cwd = os.getcwd()
4404 os.chdir(settings.GetRoot())
4405 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004406 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004407 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4408 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004409 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004411 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004412
4413 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004414 command = args + files
4415 if options.filter:
4416 command = ['--filter=' + ','.join(options.filter)] + command
4417 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004418
4419 white_regex = re.compile(settings.GetLintRegex())
4420 black_regex = re.compile(settings.GetLintIgnoreRegex())
4421 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4422 for filename in filenames:
4423 if white_regex.match(filename):
4424 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004425 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004426 else:
4427 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4428 extra_check_functions)
4429 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004430 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004431 finally:
4432 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004433 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004434 if cpplint._cpplint_state.error_count != 0:
4435 return 1
4436 return 0
4437
4438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004439def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004440 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004441 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004442 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004443 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004444 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004445 auth.add_auth_options(parser)
4446 options, args = parser.parse_args(args)
4447 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004448
sbc@chromium.org71437c02015-04-09 19:29:40 +00004449 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451 return 1
4452
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004453 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004454 if args:
4455 base_branch = args[0]
4456 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004457 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004458 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004459
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004460 cl.RunHook(
4461 committing=not options.upload,
4462 may_prompt=False,
4463 verbose=options.verbose,
4464 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004465 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466
4467
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004468def GenerateGerritChangeId(message):
4469 """Returns Ixxxxxx...xxx change id.
4470
4471 Works the same way as
4472 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4473 but can be called on demand on all platforms.
4474
4475 The basic idea is to generate git hash of a state of the tree, original commit
4476 message, author/committer info and timestamps.
4477 """
4478 lines = []
4479 tree_hash = RunGitSilent(['write-tree'])
4480 lines.append('tree %s' % tree_hash.strip())
4481 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4482 if code == 0:
4483 lines.append('parent %s' % parent.strip())
4484 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4485 lines.append('author %s' % author.strip())
4486 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4487 lines.append('committer %s' % committer.strip())
4488 lines.append('')
4489 # Note: Gerrit's commit-hook actually cleans message of some lines and
4490 # whitespace. This code is not doing this, but it clearly won't decrease
4491 # entropy.
4492 lines.append(message)
4493 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4494 stdin='\n'.join(lines))
4495 return 'I%s' % change_hash.strip()
4496
4497
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004498def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004499 """Computes the remote branch ref to use for the CL.
4500
4501 Args:
4502 remote (str): The git remote for the CL.
4503 remote_branch (str): The git remote branch for the CL.
4504 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004505 """
4506 if not (remote and remote_branch):
4507 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004508
wittman@chromium.org455dc922015-01-26 20:15:50 +00004509 if target_branch:
4510 # Cannonicalize branch references to the equivalent local full symbolic
4511 # refs, which are then translated into the remote full symbolic refs
4512 # below.
4513 if '/' not in target_branch:
4514 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4515 else:
4516 prefix_replacements = (
4517 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4518 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4519 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4520 )
4521 match = None
4522 for regex, replacement in prefix_replacements:
4523 match = re.search(regex, target_branch)
4524 if match:
4525 remote_branch = target_branch.replace(match.group(0), replacement)
4526 break
4527 if not match:
4528 # This is a branch path but not one we recognize; use as-is.
4529 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004530 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4531 # Handle the refs that need to land in different refs.
4532 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004533
wittman@chromium.org455dc922015-01-26 20:15:50 +00004534 # Create the true path to the remote branch.
4535 # Does the following translation:
4536 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4537 # * refs/remotes/origin/master -> refs/heads/master
4538 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4539 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4540 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4541 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4542 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4543 'refs/heads/')
4544 elif remote_branch.startswith('refs/remotes/branch-heads'):
4545 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004546
wittman@chromium.org455dc922015-01-26 20:15:50 +00004547 return remote_branch
4548
4549
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004550def cleanup_list(l):
4551 """Fixes a list so that comma separated items are put as individual items.
4552
4553 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4554 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4555 """
4556 items = sum((i.split(',') for i in l), [])
4557 stripped_items = (i.strip() for i in items)
4558 return sorted(filter(None, stripped_items))
4559
4560
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004561@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004562def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004563 """Uploads the current changelist to codereview.
4564
4565 Can skip dependency patchset uploads for a branch by running:
4566 git config branch.branch_name.skip-deps-uploads True
4567 To unset run:
4568 git config --unset branch.branch_name.skip-deps-uploads
4569 Can also set the above globally by using the --global flag.
4570 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004571 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4572 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004573 parser.add_option('--bypass-watchlists', action='store_true',
4574 dest='bypass_watchlists',
4575 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004576 parser.add_option('-f', action='store_true', dest='force',
4577 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004578 parser.add_option('--message', '-m', dest='message',
4579 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004580 parser.add_option('-b', '--bug',
4581 help='pre-populate the bug number(s) for this issue. '
4582 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004583 parser.add_option('--message-file', dest='message_file',
4584 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004585 parser.add_option('--title', '-t', dest='title',
4586 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004587 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004588 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004589 help='reviewer email addresses')
4590 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004591 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004592 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004593 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004594 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004595 parser.add_option('--emulate_svn_auto_props',
4596 '--emulate-svn-auto-props',
4597 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004598 dest="emulate_svn_auto_props",
4599 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004600 parser.add_option('-c', '--use-commit-queue', action='store_true',
4601 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004602 parser.add_option('--private', action='store_true',
4603 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004604 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004605 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004606 metavar='TARGET',
4607 help='Apply CL to remote ref TARGET. ' +
4608 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004609 parser.add_option('--squash', action='store_true',
4610 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004611 parser.add_option('--no-squash', action='store_true',
4612 help='Don\'t squash multiple commits into one ' +
4613 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004614 parser.add_option('--topic', default=None,
4615 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004616 parser.add_option('--email', default=None,
4617 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004618 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4619 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004620 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4621 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004622 help='Send the patchset to do a CQ dry run right after '
4623 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004624 parser.add_option('--dependencies', action='store_true',
4625 help='Uploads CLs of all the local branches that depend on '
4626 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004627
rmistry@google.com2dd99862015-06-22 12:22:18 +00004628 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004629 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004630 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004631 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004632 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004633 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004634 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004635
sbc@chromium.org71437c02015-04-09 19:29:40 +00004636 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004637 return 1
4638
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004639 options.reviewers = cleanup_list(options.reviewers)
4640 options.cc = cleanup_list(options.cc)
4641
tandriib80458a2016-06-23 12:20:07 -07004642 if options.message_file:
4643 if options.message:
4644 parser.error('only one of --message and --message-file allowed.')
4645 options.message = gclient_utils.FileRead(options.message_file)
4646 options.message_file = None
4647
tandrii4d0545a2016-07-06 03:56:49 -07004648 if options.cq_dry_run and options.use_commit_queue:
4649 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4650
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004651 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4652 settings.GetIsGerrit()
4653
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004654 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004655 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004656
4657
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004658@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004660 """DEPRECATED: Used to commit the current changelist via git-svn."""
4661 message = ('git-cl no longer supports committing to SVN repositories via '
4662 'git-svn. You probably want to use `git cl land` instead.')
4663 print(message)
4664 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665
4666
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004667@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004668def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004669 """Commits the current changelist via git.
4670
4671 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4672 upstream and closes the issue automatically and atomically.
4673
4674 Otherwise (in case of Rietveld):
4675 Squashes branch into a single commit.
4676 Updates commit message with metadata (e.g. pointer to review).
4677 Pushes the code upstream.
4678 Updates review and closes.
4679 """
4680 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4681 help='bypass upload presubmit hook')
4682 parser.add_option('-m', dest='message',
4683 help="override review description")
4684 parser.add_option('-f', action='store_true', dest='force',
4685 help="force yes to questions (don't prompt)")
4686 parser.add_option('-c', dest='contributor',
4687 help="external contributor for patch (appended to " +
4688 "description and used as author for git). Should be " +
4689 "formatted as 'First Last <email@example.com>'")
4690 add_git_similarity(parser)
4691 auth.add_auth_options(parser)
4692 (options, args) = parser.parse_args(args)
4693 auth_config = auth.extract_auth_config_from_options(options)
4694
4695 cl = Changelist(auth_config=auth_config)
4696
4697 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4698 if cl.IsGerrit():
4699 if options.message:
4700 # This could be implemented, but it requires sending a new patch to
4701 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4702 # Besides, Gerrit has the ability to change the commit message on submit
4703 # automatically, thus there is no need to support this option (so far?).
4704 parser.error('-m MESSAGE option is not supported for Gerrit.')
4705 if options.contributor:
4706 parser.error(
4707 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4708 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4709 'the contributor\'s "name <email>". If you can\'t upload such a '
4710 'commit for review, contact your repository admin and request'
4711 '"Forge-Author" permission.')
4712 if not cl.GetIssue():
4713 DieWithError('You must upload the change first to Gerrit.\n'
4714 ' If you would rather have `git cl land` upload '
4715 'automatically for you, see http://crbug.com/642759')
4716 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4717 options.verbose)
4718
4719 current = cl.GetBranch()
4720 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4721 if remote == '.':
4722 print()
4723 print('Attempting to push branch %r into another local branch!' % current)
4724 print()
4725 print('Either reparent this branch on top of origin/master:')
4726 print(' git reparent-branch --root')
4727 print()
4728 print('OR run `git rebase-update` if you think the parent branch is ')
4729 print('already committed.')
4730 print()
4731 print(' Current parent: %r' % upstream_branch)
4732 return 1
4733
4734 if not args:
4735 # Default to merging against our best guess of the upstream branch.
4736 args = [cl.GetUpstreamBranch()]
4737
4738 if options.contributor:
4739 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4740 print("Please provide contibutor as 'First Last <email@example.com>'")
4741 return 1
4742
4743 base_branch = args[0]
4744
4745 if git_common.is_dirty_git_tree('land'):
4746 return 1
4747
4748 # This rev-list syntax means "show all commits not in my branch that
4749 # are in base_branch".
4750 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4751 base_branch]).splitlines()
4752 if upstream_commits:
4753 print('Base branch "%s" has %d commits '
4754 'not in this branch.' % (base_branch, len(upstream_commits)))
4755 print('Run "git merge %s" before attempting to land.' % base_branch)
4756 return 1
4757
4758 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4759 if not options.bypass_hooks:
4760 author = None
4761 if options.contributor:
4762 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4763 hook_results = cl.RunHook(
4764 committing=True,
4765 may_prompt=not options.force,
4766 verbose=options.verbose,
4767 change=cl.GetChange(merge_base, author))
4768 if not hook_results.should_continue():
4769 return 1
4770
4771 # Check the tree status if the tree status URL is set.
4772 status = GetTreeStatus()
4773 if 'closed' == status:
4774 print('The tree is closed. Please wait for it to reopen. Use '
4775 '"git cl land --bypass-hooks" to commit on a closed tree.')
4776 return 1
4777 elif 'unknown' == status:
4778 print('Unable to determine tree status. Please verify manually and '
4779 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4780 return 1
4781
4782 change_desc = ChangeDescription(options.message)
4783 if not change_desc.description and cl.GetIssue():
4784 change_desc = ChangeDescription(cl.GetDescription())
4785
4786 if not change_desc.description:
4787 if not cl.GetIssue() and options.bypass_hooks:
4788 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4789 else:
4790 print('No description set.')
4791 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4792 return 1
4793
4794 # Keep a separate copy for the commit message, because the commit message
4795 # contains the link to the Rietveld issue, while the Rietveld message contains
4796 # the commit viewvc url.
4797 if cl.GetIssue():
4798 change_desc.update_reviewers(cl.GetApprovingReviewers())
4799
4800 commit_desc = ChangeDescription(change_desc.description)
4801 if cl.GetIssue():
4802 # Xcode won't linkify this URL unless there is a non-whitespace character
4803 # after it. Add a period on a new line to circumvent this. Also add a space
4804 # before the period to make sure that Gitiles continues to correctly resolve
4805 # the URL.
4806 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4807 if options.contributor:
4808 commit_desc.append_footer('Patch from %s.' % options.contributor)
4809
4810 print('Description:')
4811 print(commit_desc.description)
4812
4813 branches = [merge_base, cl.GetBranchRef()]
4814 if not options.force:
4815 print_stats(options.similarity, options.find_copies, branches)
4816
4817 # We want to squash all this branch's commits into one commit with the proper
4818 # description. We do this by doing a "reset --soft" to the base branch (which
4819 # keeps the working copy the same), then landing that.
4820 MERGE_BRANCH = 'git-cl-commit'
4821 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4822 # Delete the branches if they exist.
4823 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4824 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4825 result = RunGitWithCode(showref_cmd)
4826 if result[0] == 0:
4827 RunGit(['branch', '-D', branch])
4828
4829 # We might be in a directory that's present in this branch but not in the
4830 # trunk. Move up to the top of the tree so that git commands that expect a
4831 # valid CWD won't fail after we check out the merge branch.
4832 rel_base_path = settings.GetRelativeRoot()
4833 if rel_base_path:
4834 os.chdir(rel_base_path)
4835
4836 # Stuff our change into the merge branch.
4837 # We wrap in a try...finally block so if anything goes wrong,
4838 # we clean up the branches.
4839 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004840 revision = None
4841 try:
4842 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4843 RunGit(['reset', '--soft', merge_base])
4844 if options.contributor:
4845 RunGit(
4846 [
4847 'commit', '--author', options.contributor,
4848 '-m', commit_desc.description,
4849 ])
4850 else:
4851 RunGit(['commit', '-m', commit_desc.description])
4852
4853 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4854 mirror = settings.GetGitMirror(remote)
4855 if mirror:
4856 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004857 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004858 else:
4859 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004860 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004861 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4862
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004863 if git_numberer_enabled:
4864 # TODO(tandrii): maybe do autorebase + retry on failure
4865 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004866 logging.debug('Adding git number footers')
4867 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4868 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4869 branch)
4870 # Ensure timestamps are monotonically increasing.
4871 timestamp = max(1 + _get_committer_timestamp(merge_base),
4872 _get_committer_timestamp('HEAD'))
4873 _git_amend_head(commit_desc.description, timestamp)
4874 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004875
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004876 retcode, output = RunGitWithCode(
4877 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004878 if retcode == 0:
4879 revision = RunGit(['rev-parse', 'HEAD']).strip()
4880 logging.debug(output)
4881 except: # pylint: disable=bare-except
4882 if _IS_BEING_TESTED:
4883 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4884 + '-' * 30 + '8<' + '-' * 30)
4885 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4886 raise
4887 finally:
4888 # And then swap back to the original branch and clean up.
4889 RunGit(['checkout', '-q', cl.GetBranch()])
4890 RunGit(['branch', '-D', MERGE_BRANCH])
4891
4892 if not revision:
4893 print('Failed to push. If this persists, please file a bug.')
4894 return 1
4895
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004896 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004897 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004898 if viewvc_url and revision:
4899 change_desc.append_footer(
4900 'Committed: %s%s' % (viewvc_url, revision))
4901 elif revision:
4902 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004903 print('Closing issue '
4904 '(you may be prompted for your codereview password)...')
4905 cl.UpdateDescription(change_desc.description)
4906 cl.CloseIssue()
4907 props = cl.GetIssueProperties()
4908 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004909 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4910 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004911 if options.bypass_hooks:
4912 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4913 else:
4914 comment += ' (presubmit successful).'
4915 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4916
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004917 if os.path.isfile(POSTUPSTREAM_HOOK):
4918 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4919
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004920 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004921
4922
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004923@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004924def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004925 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004926 parser.add_option('-b', dest='newbranch',
4927 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004928 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004929 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004930 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4931 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004932 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004933 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004934 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004935 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004936 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004937 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004938
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004939
4940 group = optparse.OptionGroup(
4941 parser,
4942 'Options for continuing work on the current issue uploaded from a '
4943 'different clone (e.g. different machine). Must be used independently '
4944 'from the other options. No issue number should be specified, and the '
4945 'branch must have an issue number associated with it')
4946 group.add_option('--reapply', action='store_true', dest='reapply',
4947 help='Reset the branch and reapply the issue.\n'
4948 'CAUTION: This will undo any local changes in this '
4949 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004950
4951 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004952 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004953 parser.add_option_group(group)
4954
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004955 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004956 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004957 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004958 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004959 auth_config = auth.extract_auth_config_from_options(options)
4960
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004961
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004962 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004963 if options.newbranch:
4964 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004965 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004966 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004967
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004968 cl = Changelist(auth_config=auth_config,
4969 codereview=options.forced_codereview)
4970 if not cl.GetIssue():
4971 parser.error('current branch must have an associated issue')
4972
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004973 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004974 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004975 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004976
4977 RunGit(['reset', '--hard', upstream])
4978 if options.pull:
4979 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004980
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004981 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4982 options.directory)
4983
4984 if len(args) != 1 or not args[0]:
4985 parser.error('Must specify issue number or url')
4986
4987 # We don't want uncommitted changes mixed up with the patch.
4988 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004989 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004990
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004991 if options.newbranch:
4992 if options.force:
4993 RunGit(['branch', '-D', options.newbranch],
4994 stderr=subprocess2.PIPE, error_ok=True)
4995 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004996 elif not GetCurrentBranch():
4997 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004998
4999 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
5000
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005001 if cl.IsGerrit():
5002 if options.reject:
5003 parser.error('--reject is not supported with Gerrit codereview.')
5004 if options.nocommit:
5005 parser.error('--nocommit is not supported with Gerrit codereview.')
5006 if options.directory:
5007 parser.error('--directory is not supported with Gerrit codereview.')
5008
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005009 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005010 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005011
5012
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005013def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005014 """Fetches the tree status and returns either 'open', 'closed',
5015 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005016 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005017 if url:
5018 status = urllib2.urlopen(url).read().lower()
5019 if status.find('closed') != -1 or status == '0':
5020 return 'closed'
5021 elif status.find('open') != -1 or status == '1':
5022 return 'open'
5023 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005024 return 'unset'
5025
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005026
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005027def GetTreeStatusReason():
5028 """Fetches the tree status from a json url and returns the message
5029 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005030 url = settings.GetTreeStatusUrl()
5031 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005032 connection = urllib2.urlopen(json_url)
5033 status = json.loads(connection.read())
5034 connection.close()
5035 return status['message']
5036
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005038def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005039 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005040 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005041 status = GetTreeStatus()
5042 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005043 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005044 return 2
5045
vapiera7fbd5a2016-06-16 09:17:49 -07005046 print('The tree is %s' % status)
5047 print()
5048 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005049 if status != 'open':
5050 return 1
5051 return 0
5052
5053
maruel@chromium.org15192402012-09-06 12:38:29 +00005054def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005055 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005056 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005057 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005058 '-b', '--bot', action='append',
5059 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5060 'times to specify multiple builders. ex: '
5061 '"-b win_rel -b win_layout". See '
5062 'the try server waterfall for the builders name and the tests '
5063 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005064 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005065 '-B', '--bucket', default='',
5066 help=('Buildbucket bucket to send the try requests.'))
5067 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005068 '-m', '--master', default='',
5069 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005070 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005071 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005072 help='Revision to use for the try job; default: the revision will '
5073 'be determined by the try recipe that builder runs, which usually '
5074 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005075 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005076 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005077 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005078 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005079 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005080 '--project',
5081 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005082 'in recipe to determine to which repository or directory to '
5083 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005084 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005085 '-p', '--property', dest='properties', action='append', default=[],
5086 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005087 'key2=value2 etc. The value will be treated as '
5088 'json if decodable, or as string otherwise. '
5089 'NOTE: using this may make your try job not usable for CQ, '
5090 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005091 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005092 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5093 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005094 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005095 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005096 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005097 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005098
machenbach@chromium.org45453142015-09-15 08:45:22 +00005099 # Make sure that all properties are prop=value pairs.
5100 bad_params = [x for x in options.properties if '=' not in x]
5101 if bad_params:
5102 parser.error('Got properties with missing "=": %s' % bad_params)
5103
maruel@chromium.org15192402012-09-06 12:38:29 +00005104 if args:
5105 parser.error('Unknown arguments: %s' % args)
5106
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005107 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005108 if not cl.GetIssue():
5109 parser.error('Need to upload first')
5110
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005111 if cl.IsGerrit():
5112 # HACK: warm up Gerrit change detail cache to save on RPCs.
5113 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5114
tandriie113dfd2016-10-11 10:20:12 -07005115 error_message = cl.CannotTriggerTryJobReason()
5116 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005117 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005118
borenet6c0efe62016-10-19 08:13:29 -07005119 if options.bucket and options.master:
5120 parser.error('Only one of --bucket and --master may be used.')
5121
qyearsley1fdfcb62016-10-24 13:22:03 -07005122 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005123
qyearsleydd49f942016-10-28 11:57:22 -07005124 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5125 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005126 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005127 if options.verbose:
5128 print('git cl try with no bots now defaults to CQ Dry Run.')
5129 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005130
borenet6c0efe62016-10-19 08:13:29 -07005131 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005132 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005133 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005134 'of bot requires an initial job from a parent (usually a builder). '
5135 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005136 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005137 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005138
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005139 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005140 # TODO(tandrii): Checking local patchset against remote patchset is only
5141 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5142 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005143 print('Warning: Codereview server has newer patchsets (%s) than most '
5144 'recent upload from local checkout (%s). Did a previous upload '
5145 'fail?\n'
5146 'By default, git cl try uses the latest patchset from '
5147 'codereview, continuing to use patchset %s.\n' %
5148 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005149
tandrii568043b2016-10-11 07:49:18 -07005150 try:
borenet6c0efe62016-10-19 08:13:29 -07005151 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5152 patchset)
tandrii568043b2016-10-11 07:49:18 -07005153 except BuildbucketResponseException as ex:
5154 print('ERROR: %s' % ex)
5155 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005156 return 0
5157
5158
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005159def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005160 """Prints info about try jobs associated with current CL."""
5161 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005162 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005163 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005164 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005165 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005166 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005167 '--color', action='store_true', default=setup_color.IS_TTY,
5168 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005169 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005170 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5171 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005172 group.add_option(
5173 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005174 parser.add_option_group(group)
5175 auth.add_auth_options(parser)
5176 options, args = parser.parse_args(args)
5177 if args:
5178 parser.error('Unrecognized args: %s' % ' '.join(args))
5179
5180 auth_config = auth.extract_auth_config_from_options(options)
5181 cl = Changelist(auth_config=auth_config)
5182 if not cl.GetIssue():
5183 parser.error('Need to upload first')
5184
tandrii221ab252016-10-06 08:12:04 -07005185 patchset = options.patchset
5186 if not patchset:
5187 patchset = cl.GetMostRecentPatchset()
5188 if not patchset:
5189 parser.error('Codereview doesn\'t know about issue %s. '
5190 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005191 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005192 cl.GetIssue())
5193
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005194 # TODO(tandrii): Checking local patchset against remote patchset is only
5195 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5196 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005197 print('Warning: Codereview server has newer patchsets (%s) than most '
5198 'recent upload from local checkout (%s). Did a previous upload '
5199 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005200 'By default, git cl try-results uses the latest patchset from '
5201 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005202 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005203 try:
tandrii221ab252016-10-06 08:12:04 -07005204 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005205 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005206 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005207 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005208 if options.json:
5209 write_try_results_json(options.json, jobs)
5210 else:
5211 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005212 return 0
5213
5214
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005215@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005216def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005217 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005218 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005219 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005220 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005221
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005222 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005223 if args:
5224 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005225 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005226 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005227 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005228 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005229
5230 # Clear configured merge-base, if there is one.
5231 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005232 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005233 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005234 return 0
5235
5236
thestig@chromium.org00858c82013-12-02 23:08:03 +00005237def CMDweb(parser, args):
5238 """Opens the current CL in the web browser."""
5239 _, args = parser.parse_args(args)
5240 if args:
5241 parser.error('Unrecognized args: %s' % ' '.join(args))
5242
5243 issue_url = Changelist().GetIssueURL()
5244 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005245 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005246 return 1
5247
5248 webbrowser.open(issue_url)
5249 return 0
5250
5251
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005252def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005253 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005254 parser.add_option('-d', '--dry-run', action='store_true',
5255 help='trigger in dry run mode')
5256 parser.add_option('-c', '--clear', action='store_true',
5257 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005258 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005259 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005260 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005261 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005262 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005263 if args:
5264 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005265 if options.dry_run and options.clear:
5266 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5267
iannuccie53c9352016-08-17 14:40:40 -07005268 cl = Changelist(auth_config=auth_config, issue=options.issue,
5269 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005270 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005271 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005272 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005273 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005274 state = _CQState.DRY_RUN
5275 else:
5276 state = _CQState.COMMIT
5277 if not cl.GetIssue():
5278 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005279 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005280 return 0
5281
5282
groby@chromium.org411034a2013-02-26 15:12:01 +00005283def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005284 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005285 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005286 auth.add_auth_options(parser)
5287 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005288 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005289 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005290 if args:
5291 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005292 cl = Changelist(auth_config=auth_config, issue=options.issue,
5293 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005294 # Ensure there actually is an issue to close.
5295 cl.GetDescription()
5296 cl.CloseIssue()
5297 return 0
5298
5299
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005300def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005301 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005302 parser.add_option(
5303 '--stat',
5304 action='store_true',
5305 dest='stat',
5306 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005307 auth.add_auth_options(parser)
5308 options, args = parser.parse_args(args)
5309 auth_config = auth.extract_auth_config_from_options(options)
5310 if args:
5311 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005312
5313 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005314 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005315 # Staged changes would be committed along with the patch from last
5316 # upload, hence counted toward the "last upload" side in the final
5317 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005318 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005319 return 1
5320
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005321 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005322 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005323 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005324 if not issue:
5325 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005326 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005327 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005328
5329 # Create a new branch based on the merge-base
5330 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005331 # Clear cached branch in cl object, to avoid overwriting original CL branch
5332 # properties.
5333 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005334 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005335 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005336 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005337 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005338 return rtn
5339
wychen@chromium.org06928532015-02-03 02:11:29 +00005340 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005341 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005342 cmd = ['git', 'diff']
5343 if options.stat:
5344 cmd.append('--stat')
5345 cmd.extend([TMP_BRANCH, branch, '--'])
5346 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005347 finally:
5348 RunGit(['checkout', '-q', branch])
5349 RunGit(['branch', '-D', TMP_BRANCH])
5350
5351 return 0
5352
5353
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005354def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005355 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005356 parser.add_option(
5357 '--no-color',
5358 action='store_true',
5359 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005360 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005361 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005362 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005363
5364 author = RunGit(['config', 'user.email']).strip() or None
5365
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005366 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005367
5368 if args:
5369 if len(args) > 1:
5370 parser.error('Unknown args')
5371 base_branch = args[0]
5372 else:
5373 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005374 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005375
5376 change = cl.GetChange(base_branch, None)
5377 return owners_finder.OwnersFinder(
5378 [f.LocalPath() for f in
5379 cl.GetChange(base_branch, None).AffectedFiles()],
5380 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005381 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005382 disable_color=options.no_color).run()
5383
5384
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005385def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005386 """Generates a diff command."""
5387 # Generate diff for the current branch's changes.
5388 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005389 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005390
5391 if args:
5392 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005393 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005394 diff_cmd.append(arg)
5395 else:
5396 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005397
5398 return diff_cmd
5399
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005400
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005401def MatchingFileType(file_name, extensions):
5402 """Returns true if the file name ends with one of the given extensions."""
5403 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005404
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005405
enne@chromium.org555cfe42014-01-29 18:21:39 +00005406@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005407def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005408 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005409 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005410 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005411 parser.add_option('--full', action='store_true',
5412 help='Reformat the full content of all touched files')
5413 parser.add_option('--dry-run', action='store_true',
5414 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005415 parser.add_option('--python', action='store_true',
5416 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005417 parser.add_option('--js', action='store_true',
5418 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005419 parser.add_option('--diff', action='store_true',
5420 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005421 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005422
Daniel Chengc55eecf2016-12-30 03:11:02 -08005423 # Normalize any remaining args against the current path, so paths relative to
5424 # the current directory are still resolved as expected.
5425 args = [os.path.join(os.getcwd(), arg) for arg in args]
5426
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005427 # git diff generates paths against the root of the repository. Change
5428 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005429 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005430 if rel_base_path:
5431 os.chdir(rel_base_path)
5432
digit@chromium.org29e47272013-05-17 17:01:46 +00005433 # Grab the merge-base commit, i.e. the upstream commit of the current
5434 # branch when it was created or the last time it was rebased. This is
5435 # to cover the case where the user may have called "git fetch origin",
5436 # moving the origin branch to a newer commit, but hasn't rebased yet.
5437 upstream_commit = None
5438 cl = Changelist()
5439 upstream_branch = cl.GetUpstreamBranch()
5440 if upstream_branch:
5441 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5442 upstream_commit = upstream_commit.strip()
5443
5444 if not upstream_commit:
5445 DieWithError('Could not find base commit for this branch. '
5446 'Are you in detached state?')
5447
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005448 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5449 diff_output = RunGit(changed_files_cmd)
5450 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005451 # Filter out files deleted by this CL
5452 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005453
Christopher Lamc5ba6922017-01-24 11:19:14 +11005454 if opts.js:
5455 CLANG_EXTS.append('.js')
5456
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005457 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5458 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5459 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005460 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005461
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005462 top_dir = os.path.normpath(
5463 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5464
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005465 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5466 # formatted. This is used to block during the presubmit.
5467 return_value = 0
5468
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005469 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005470 # Locate the clang-format binary in the checkout
5471 try:
5472 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005473 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005474 DieWithError(e)
5475
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005476 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005477 cmd = [clang_format_tool]
5478 if not opts.dry_run and not opts.diff:
5479 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005480 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005481 if opts.diff:
5482 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005483 else:
5484 env = os.environ.copy()
5485 env['PATH'] = str(os.path.dirname(clang_format_tool))
5486 try:
5487 script = clang_format.FindClangFormatScriptInChromiumTree(
5488 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005489 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005490 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005491
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005492 cmd = [sys.executable, script, '-p0']
5493 if not opts.dry_run and not opts.diff:
5494 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005495
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005496 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5497 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005498
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005499 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5500 if opts.diff:
5501 sys.stdout.write(stdout)
5502 if opts.dry_run and len(stdout) > 0:
5503 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005504
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005505 # Similar code to above, but using yapf on .py files rather than clang-format
5506 # on C/C++ files
5507 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005508 yapf_tool = gclient_utils.FindExecutable('yapf')
5509 if yapf_tool is None:
5510 DieWithError('yapf not found in PATH')
5511
5512 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005513 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005514 cmd = [yapf_tool]
5515 if not opts.dry_run and not opts.diff:
5516 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005517 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005518 if opts.diff:
5519 sys.stdout.write(stdout)
5520 else:
5521 # TODO(sbc): yapf --lines mode still has some issues.
5522 # https://github.com/google/yapf/issues/154
5523 DieWithError('--python currently only works with --full')
5524
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005525 # Dart's formatter does not have the nice property of only operating on
5526 # modified chunks, so hard code full.
5527 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005528 try:
5529 command = [dart_format.FindDartFmtToolInChromiumTree()]
5530 if not opts.dry_run and not opts.diff:
5531 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005532 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005533
ppi@chromium.org6593d932016-03-03 15:41:15 +00005534 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005535 if opts.dry_run and stdout:
5536 return_value = 2
5537 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005538 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5539 'found in this checkout. Files in other languages are still '
5540 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005541
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005542 # Format GN build files. Always run on full build files for canonical form.
5543 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005544 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005545 if opts.dry_run or opts.diff:
5546 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005547 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005548 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5549 shell=sys.platform == 'win32',
5550 cwd=top_dir)
5551 if opts.dry_run and gn_ret == 2:
5552 return_value = 2 # Not formatted.
5553 elif opts.diff and gn_ret == 2:
5554 # TODO this should compute and print the actual diff.
5555 print("This change has GN build file diff for " + gn_diff_file)
5556 elif gn_ret != 0:
5557 # For non-dry run cases (and non-2 return values for dry-run), a
5558 # nonzero error code indicates a failure, probably because the file
5559 # doesn't parse.
5560 DieWithError("gn format failed on " + gn_diff_file +
5561 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005562
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005563 metrics_xml_files = [
5564 'tools/metrics/actions/actions.xml',
5565 'tools/metrics/histograms/histograms.xml',
5566 'tools/metrics/rappor/rappor.xml']
5567 for xml_file in metrics_xml_files:
5568 if xml_file in diff_files:
5569 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5570 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5571 if opts.dry_run or opts.diff:
5572 cmd.append('--diff')
5573 stdout = RunCommand(cmd, cwd=top_dir)
5574 if opts.diff:
5575 sys.stdout.write(stdout)
5576 if opts.dry_run and stdout:
5577 return_value = 2 # Not formatted.
5578
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005579 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005580
5581
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005582@subcommand.usage('<codereview url or issue id>')
5583def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005584 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005585 _, args = parser.parse_args(args)
5586
5587 if len(args) != 1:
5588 parser.print_help()
5589 return 1
5590
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005591 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005592 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005593 parser.print_help()
5594 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005595 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005596
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005597 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005598 output = RunGit(['config', '--local', '--get-regexp',
5599 r'branch\..*\.%s' % issueprefix],
5600 error_ok=True)
5601 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005602 if issue == target_issue:
5603 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005604
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005605 branches = []
5606 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005607 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005608 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005609 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005610 return 1
5611 if len(branches) == 1:
5612 RunGit(['checkout', branches[0]])
5613 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005614 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005615 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005616 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005617 which = raw_input('Choose by index: ')
5618 try:
5619 RunGit(['checkout', branches[int(which)]])
5620 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005621 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005622 return 1
5623
5624 return 0
5625
5626
maruel@chromium.org29404b52014-09-08 22:58:00 +00005627def CMDlol(parser, args):
5628 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005629 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005630 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5631 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5632 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005633 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005634 return 0
5635
5636
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005637class OptionParser(optparse.OptionParser):
5638 """Creates the option parse and add --verbose support."""
5639 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005640 optparse.OptionParser.__init__(
5641 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005642 self.add_option(
5643 '-v', '--verbose', action='count', default=0,
5644 help='Use 2 times for more debugging info')
5645
5646 def parse_args(self, args=None, values=None):
5647 options, args = optparse.OptionParser.parse_args(self, args, values)
5648 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005649 logging.basicConfig(
5650 level=levels[min(options.verbose, len(levels) - 1)],
5651 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5652 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005653 return options, args
5654
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005656def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005657 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005658 print('\nYour python version %s is unsupported, please upgrade.\n' %
5659 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005660 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005661
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005662 # Reload settings.
5663 global settings
5664 settings = Settings()
5665
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005666 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005667 dispatcher = subcommand.CommandDispatcher(__name__)
5668 try:
5669 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005670 except auth.AuthenticationError as e:
5671 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005672 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005673 if e.code != 500:
5674 raise
5675 DieWithError(
5676 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5677 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005678 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005679
5680
5681if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005682 # These affect sys.stdout so do it outside of main() to simplify mocks in
5683 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005684 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005685 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005686 try:
5687 sys.exit(main(sys.argv[1:]))
5688 except KeyboardInterrupt:
5689 sys.stderr.write('interrupted\n')
5690 sys.exit(1)