blob: 342d82c11c30e2b4134e329e5b87b843f92ba313 [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 Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010019import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000022import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import optparse
24import os
25import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010026import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000027import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000032import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000034import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000035import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036
37try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080038 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039except ImportError:
40 pass
41
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000042from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000043from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000045import auth
skobes6468b902016-10-24 08:45:10 -070046import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000047import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070068DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080069POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
borenet6c0efe62016-10-19 08:13:29 -070080# Buildbucket master name prefix.
81MASTER_PREFIX = 'master.'
82
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000083# Shortcut since it quickly becomes redundant.
84Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000085
maruel@chromium.orgddd59412011-11-30 14:20:38 +000086# Initialized in main()
87settings = None
88
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010089# Used by tests/git_cl_test.py to add extra logging.
90# Inside the weirdly failing test, add this:
91# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
92# And scroll up to see the strack trace printed.
93_IS_BEING_TESTED = False
94
maruel@chromium.orgddd59412011-11-30 14:20:38 +000095
Christopher Lamf732cd52017-01-24 12:40:11 +110096def DieWithError(message, change_desc=None):
97 if change_desc:
98 SaveDescriptionBackup(change_desc)
99
vapiera7fbd5a2016-06-16 09:17:49 -0700100 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 sys.exit(1)
102
103
Christopher Lamf732cd52017-01-24 12:40:11 +1100104def SaveDescriptionBackup(change_desc):
105 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
106 print('\nError after CL description prompt -- saving description to %s\n' %
107 backup_path)
108 backup_file = open(backup_path, 'w')
109 backup_file.write(change_desc.description)
110 backup_file.close()
111
112
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000113def GetNoGitPagerEnv():
114 env = os.environ.copy()
115 # 'cat' is a magical git string that disables pagers on all platforms.
116 env['GIT_PAGER'] = 'cat'
117 return env
118
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000119
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000121 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000122 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000123 except subprocess2.CalledProcessError as e:
124 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000127 'Command "%s" failed.\n%s' % (
128 ' '.join(args), error_message or e.stdout or ''))
129 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
132def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000133 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000134 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000135
136
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000137def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700139 if suppress_stderr:
140 stderr = subprocess2.VOID
141 else:
142 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000143 try:
tandrii5d48c322016-08-18 16:19:37 -0700144 (out, _), code = subprocess2.communicate(['git'] + args,
145 env=GetNoGitPagerEnv(),
146 stdout=subprocess2.PIPE,
147 stderr=stderr)
148 return code, out
149 except subprocess2.CalledProcessError as e:
150 logging.debug('Failed running %s', args)
151 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000152
153
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000155 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000156 return RunGitWithCode(args, suppress_stderr=True)[1]
157
158
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000161 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000162 return (version.startswith(prefix) and
163 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164
165
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000166def BranchExists(branch):
167 """Return True if specified branch exists."""
168 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
169 suppress_stderr=True)
170 return not code
171
172
tandrii2a16b952016-10-19 07:09:44 -0700173def time_sleep(seconds):
174 # Use this so that it can be mocked in tests without interfering with python
175 # system machinery.
176 import time # Local import to discourage others from importing time globally.
177 return time.sleep(seconds)
178
179
maruel@chromium.org90541732011-04-01 17:54:18 +0000180def ask_for_data(prompt):
181 try:
182 return raw_input(prompt)
183 except KeyboardInterrupt:
184 # Hide the exception.
185 sys.exit(1)
186
187
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100188def confirm_or_exit(prefix='', action='confirm'):
189 """Asks user to press enter to continue or press Ctrl+C to abort."""
190 if not prefix or prefix.endswith('\n'):
191 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100192 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100193 mid = ' Press'
194 elif prefix.endswith(' '):
195 mid = 'press'
196 else:
197 mid = ' press'
198 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
199
200
201def ask_for_explicit_yes(prompt):
202 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
203 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
204 while True:
205 if 'yes'.startswith(result):
206 return True
207 if 'no'.startswith(result):
208 return False
209 result = ask_for_data('Please, type yes or no: ').lower()
210
211
tandrii5d48c322016-08-18 16:19:37 -0700212def _git_branch_config_key(branch, key):
213 """Helper method to return Git config key for a branch."""
214 assert branch, 'branch name is required to set git config for it'
215 return 'branch.%s.%s' % (branch, key)
216
217
218def _git_get_branch_config_value(key, default=None, value_type=str,
219 branch=False):
220 """Returns git config value of given or current branch if any.
221
222 Returns default in all other cases.
223 """
224 assert value_type in (int, str, bool)
225 if branch is False: # Distinguishing default arg value from None.
226 branch = GetCurrentBranch()
227
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000228 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700229 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000230
tandrii5d48c322016-08-18 16:19:37 -0700231 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700232 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700233 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700234 # git config also has --int, but apparently git config suffers from integer
235 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append(_git_branch_config_key(branch, key))
237 code, out = RunGitWithCode(args)
238 if code == 0:
239 value = out.strip()
240 if value_type == int:
241 return int(value)
242 if value_type == bool:
243 return bool(value.lower() == 'true')
244 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 return default
246
247
tandrii5d48c322016-08-18 16:19:37 -0700248def _git_set_branch_config_value(key, value, branch=None, **kwargs):
249 """Sets the value or unsets if it's None of a git branch config.
250
251 Valid, though not necessarily existing, branch must be provided,
252 otherwise currently checked out branch is used.
253 """
254 if not branch:
255 branch = GetCurrentBranch()
256 assert branch, 'a branch name OR currently checked out branch is required'
257 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700258 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700259 if value is None:
260 args.append('--unset')
261 elif isinstance(value, bool):
262 args.append('--bool')
263 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700264 else:
tandrii33a46ff2016-08-23 05:53:40 -0700265 # git config also has --int, but apparently git config suffers from integer
266 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700267 value = str(value)
268 args.append(_git_branch_config_key(branch, key))
269 if value is not None:
270 args.append(value)
271 RunGit(args, **kwargs)
272
273
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100274def _get_committer_timestamp(commit):
275 """Returns unix timestamp as integer of a committer in a commit.
276
277 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
278 """
279 # Git also stores timezone offset, but it only affects visual display,
280 # actual point in time is defined by this timestamp only.
281 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
282
283
284def _git_amend_head(message, committer_timestamp):
285 """Amends commit with new message and desired committer_timestamp.
286
287 Sets committer timezone to UTC.
288 """
289 env = os.environ.copy()
290 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
291 return RunGit(['commit', '--amend', '-m', message], env=env)
292
293
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000294def add_git_similarity(parser):
295 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700296 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000297 help='Sets the percentage that a pair of files need to match in order to'
298 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000299 parser.add_option(
300 '--find-copies', action='store_true',
301 help='Allows git to look for copies.')
302 parser.add_option(
303 '--no-find-copies', action='store_false', dest='find_copies',
304 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000305
306 old_parser_args = parser.parse_args
307 def Parse(args):
308 options, args = old_parser_args(args)
309
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000310 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700311 options.similarity = _git_get_branch_config_value(
312 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000313 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000314 print('Note: Saving similarity of %d%% in git config.'
315 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700316 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000317
iannucci@chromium.org79540052012-10-19 23:15:26 +0000318 options.similarity = max(0, min(options.similarity, 100))
319
320 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700321 options.find_copies = _git_get_branch_config_value(
322 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000323 else:
tandrii5d48c322016-08-18 16:19:37 -0700324 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000325
326 print('Using %d%% similarity for rename/copy detection. '
327 'Override with --similarity.' % options.similarity)
328
329 return options, args
330 parser.parse_args = Parse
331
332
machenbach@chromium.org45453142015-09-15 08:45:22 +0000333def _get_properties_from_options(options):
334 properties = dict(x.split('=', 1) for x in options.properties)
335 for key, val in properties.iteritems():
336 try:
337 properties[key] = json.loads(val)
338 except ValueError:
339 pass # If a value couldn't be evaluated, treat it as a string.
340 return properties
341
342
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000343def _prefix_master(master):
344 """Convert user-specified master name to full master name.
345
346 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
347 name, while the developers always use shortened master name
348 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
349 function does the conversion for buildbucket migration.
350 """
borenet6c0efe62016-10-19 08:13:29 -0700351 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000352 return master
borenet6c0efe62016-10-19 08:13:29 -0700353 return '%s%s' % (MASTER_PREFIX, master)
354
355
356def _unprefix_master(bucket):
357 """Convert bucket name to shortened master name.
358
359 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
360 name, while the developers always use shortened master name
361 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
362 function does the conversion for buildbucket migration.
363 """
364 if bucket.startswith(MASTER_PREFIX):
365 return bucket[len(MASTER_PREFIX):]
366 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367
368
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369def _buildbucket_retry(operation_name, http, *args, **kwargs):
370 """Retries requests to buildbucket service and returns parsed json content."""
371 try_count = 0
372 while True:
373 response, content = http.request(*args, **kwargs)
374 try:
375 content_json = json.loads(content)
376 except ValueError:
377 content_json = None
378
379 # Buildbucket could return an error even if status==200.
380 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000381 error = content_json.get('error')
382 if error.get('code') == 403:
383 raise BuildbucketResponseException(
384 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000386 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 raise BuildbucketResponseException(msg)
388
389 if response.status == 200:
390 if not content_json:
391 raise BuildbucketResponseException(
392 'Buildbucket returns invalid json content: %s.\n'
393 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
394 content)
395 return content_json
396 if response.status < 500 or try_count >= 2:
397 raise httplib2.HttpLib2Error(content)
398
399 # status >= 500 means transient failures.
400 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700401 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000402 try_count += 1
403 assert False, 'unreachable'
404
405
qyearsley1fdfcb62016-10-24 13:22:03 -0700406def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700407 """Returns a dict mapping bucket names to builders and tests,
408 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 """
qyearsleydd49f942016-10-28 11:57:22 -0700410 # If no bots are listed, we try to get a set of builders and tests based
411 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700412 if not options.bot:
413 change = changelist.GetChange(
414 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700415 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700416 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700417 change=change,
418 changed_files=change.LocalPaths(),
419 repository_root=settings.GetRoot(),
420 default_presubmit=None,
421 project=None,
422 verbose=options.verbose,
423 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700424 if masters is None:
425 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100426 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700427
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 if options.bucket:
429 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700430 if options.master:
431 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
qyearsleydd49f942016-10-28 11:57:22 -0700433 # If bots are listed but no master or bucket, then we need to find out
434 # the corresponding master for each bot.
435 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
436 if error_message:
437 option_parser.error(
438 'Tryserver master cannot be found because: %s\n'
439 'Please manually specify the tryserver master, e.g. '
440 '"-m tryserver.chromium.linux".' % error_message)
441 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700442
443
qyearsley123a4682016-10-26 09:12:17 -0700444def _get_bucket_map_for_builders(builders):
445 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700446 map_url = 'https://builders-map.appspot.com/'
447 try:
qyearsley123a4682016-10-26 09:12:17 -0700448 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700449 except urllib2.URLError as e:
450 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
451 (map_url, e))
452 except ValueError as e:
453 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700454 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700455 return None, 'Failed to build master map.'
456
qyearsley123a4682016-10-26 09:12:17 -0700457 bucket_map = {}
458 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700459 masters = builders_map.get(builder, [])
460 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700461 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700462 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700463 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700464 (builder, masters))
465 bucket = _prefix_master(masters[0])
466 bucket_map.setdefault(bucket, {})[builder] = []
467
468 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700469
470
borenet6c0efe62016-10-19 08:13:29 -0700471def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700472 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700473 """Sends a request to Buildbucket to trigger try jobs for a changelist.
474
475 Args:
476 auth_config: AuthConfig for Rietveld.
477 changelist: Changelist that the try jobs are associated with.
478 buckets: A nested dict mapping bucket names to builders to tests.
479 options: Command-line options.
480 """
tandriide281ae2016-10-12 06:02:30 -0700481 assert changelist.GetIssue(), 'CL must be uploaded first'
482 codereview_url = changelist.GetCodereviewServer()
483 assert codereview_url, 'CL must be uploaded first'
484 patchset = patchset or changelist.GetMostRecentPatchset()
485 assert patchset, 'CL must be uploaded first'
486
487 codereview_host = urlparse.urlparse(codereview_url).hostname
488 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 http = authenticator.authorize(httplib2.Http())
490 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700491
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492 buildbucket_put_url = (
493 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000494 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700495 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
496 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
497 hostname=codereview_host,
498 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000499 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700500
501 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
502 shared_parameters_properties['category'] = category
503 if options.clobber:
504 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700505 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700506 if extra_properties:
507 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000508
509 batch_req_body = {'builds': []}
510 print_text = []
511 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700512 for bucket, builders_and_tests in sorted(buckets.iteritems()):
513 print_text.append('Bucket: %s' % bucket)
514 master = None
515 if bucket.startswith(MASTER_PREFIX):
516 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000517 for builder, tests in sorted(builders_and_tests.iteritems()):
518 print_text.append(' %s: %s' % (builder, tests))
519 parameters = {
520 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000521 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100522 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000523 'revision': options.revision,
524 }],
tandrii8c5a3532016-11-04 07:52:02 -0700525 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000526 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000527 if 'presubmit' in builder.lower():
528 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000529 if tests:
530 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700531
532 tags = [
533 'builder:%s' % builder,
534 'buildset:%s' % buildset,
535 'user_agent:git_cl_try',
536 ]
537 if master:
538 parameters['properties']['master'] = master
539 tags.append('master:%s' % master)
540
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000541 batch_req_body['builds'].append(
542 {
543 'bucket': bucket,
544 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700546 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000547 }
548 )
549
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700551 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 http,
553 buildbucket_put_url,
554 'PUT',
555 body=json.dumps(batch_req_body),
556 headers={'Content-Type': 'application/json'}
557 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000558 print_text.append('To see results here, run: git cl try-results')
559 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700560 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000561
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000562
tandrii221ab252016-10-06 08:12:04 -0700563def fetch_try_jobs(auth_config, changelist, buildbucket_host,
564 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700565 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566
qyearsley53f48a12016-09-01 10:45:13 -0700567 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 """
tandrii221ab252016-10-06 08:12:04 -0700569 assert buildbucket_host
570 assert changelist.GetIssue(), 'CL must be uploaded first'
571 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
572 patchset = patchset or changelist.GetMostRecentPatchset()
573 assert patchset, 'CL must be uploaded first'
574
575 codereview_url = changelist.GetCodereviewServer()
576 codereview_host = urlparse.urlparse(codereview_url).hostname
577 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 if authenticator.has_cached_credentials():
579 http = authenticator.authorize(httplib2.Http())
580 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700581 print('Warning: Some results might be missing because %s' %
582 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700583 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584 http = httplib2.Http()
585
586 http.force_exception_to_status_code = True
587
tandrii221ab252016-10-06 08:12:04 -0700588 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
589 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
590 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700592 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 params = {'tag': 'buildset:%s' % buildset}
594
595 builds = {}
596 while True:
597 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700598 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700600 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 for build in content.get('builds', []):
602 builds[build['id']] = build
603 if 'next_cursor' in content:
604 params['start_cursor'] = content['next_cursor']
605 else:
606 break
607 return builds
608
609
qyearsleyeab3c042016-08-24 09:18:28 -0700610def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611 """Prints nicely result of fetch_try_jobs."""
612 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700613 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614 return
615
616 # Make a copy, because we'll be modifying builds dictionary.
617 builds = builds.copy()
618 builder_names_cache = {}
619
620 def get_builder(b):
621 try:
622 return builder_names_cache[b['id']]
623 except KeyError:
624 try:
625 parameters = json.loads(b['parameters_json'])
626 name = parameters['builder_name']
627 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700628 print('WARNING: failed to get builder name for build %s: %s' % (
629 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 name = None
631 builder_names_cache[b['id']] = name
632 return name
633
634 def get_bucket(b):
635 bucket = b['bucket']
636 if bucket.startswith('master.'):
637 return bucket[len('master.'):]
638 return bucket
639
640 if options.print_master:
641 name_fmt = '%%-%ds %%-%ds' % (
642 max(len(str(get_bucket(b))) for b in builds.itervalues()),
643 max(len(str(get_builder(b))) for b in builds.itervalues()))
644 def get_name(b):
645 return name_fmt % (get_bucket(b), get_builder(b))
646 else:
647 name_fmt = '%%-%ds' % (
648 max(len(str(get_builder(b))) for b in builds.itervalues()))
649 def get_name(b):
650 return name_fmt % get_builder(b)
651
652 def sort_key(b):
653 return b['status'], b.get('result'), get_name(b), b.get('url')
654
655 def pop(title, f, color=None, **kwargs):
656 """Pop matching builds from `builds` dict and print them."""
657
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000658 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000659 colorize = str
660 else:
661 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
662
663 result = []
664 for b in builds.values():
665 if all(b.get(k) == v for k, v in kwargs.iteritems()):
666 builds.pop(b['id'])
667 result.append(b)
668 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700669 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700671 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673 total = len(builds)
674 pop(status='COMPLETED', result='SUCCESS',
675 title='Successes:', color=Fore.GREEN,
676 f=lambda b: (get_name(b), b.get('url')))
677 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
678 title='Infra Failures:', color=Fore.MAGENTA,
679 f=lambda b: (get_name(b), b.get('url')))
680 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
681 title='Failures:', color=Fore.RED,
682 f=lambda b: (get_name(b), b.get('url')))
683 pop(status='COMPLETED', result='CANCELED',
684 title='Canceled:', color=Fore.MAGENTA,
685 f=lambda b: (get_name(b),))
686 pop(status='COMPLETED', result='FAILURE',
687 failure_reason='INVALID_BUILD_DEFINITION',
688 title='Wrong master/builder name:', color=Fore.MAGENTA,
689 f=lambda b: (get_name(b),))
690 pop(status='COMPLETED', result='FAILURE',
691 title='Other failures:',
692 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
693 pop(status='COMPLETED',
694 title='Other finished:',
695 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
696 pop(status='STARTED',
697 title='Started:', color=Fore.YELLOW,
698 f=lambda b: (get_name(b), b.get('url')))
699 pop(status='SCHEDULED',
700 title='Scheduled:',
701 f=lambda b: (get_name(b), 'id=%s' % b['id']))
702 # The last section is just in case buildbucket API changes OR there is a bug.
703 pop(title='Other:',
704 f=lambda b: (get_name(b), 'id=%s' % b['id']))
705 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700706 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000707
708
qyearsley53f48a12016-09-01 10:45:13 -0700709def write_try_results_json(output_file, builds):
710 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
711
712 The input |builds| dict is assumed to be generated by Buildbucket.
713 Buildbucket documentation: http://goo.gl/G0s101
714 """
715
716 def convert_build_dict(build):
717 return {
718 'buildbucket_id': build.get('id'),
719 'status': build.get('status'),
720 'result': build.get('result'),
721 'bucket': build.get('bucket'),
722 'builder_name': json.loads(
723 build.get('parameters_json', '{}')).get('builder_name'),
724 'failure_reason': build.get('failure_reason'),
725 'url': build.get('url'),
726 }
727
728 converted = []
729 for _, build in sorted(builds.items()):
730 converted.append(convert_build_dict(build))
731 write_json(output_file, converted)
732
733
iannucci@chromium.org79540052012-10-19 23:15:26 +0000734def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000735 """Prints statistics about the change to the user."""
736 # --no-ext-diff is broken in some versions of Git, so try to work around
737 # this by overriding the environment (but there is still a problem if the
738 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000739 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000740 if 'GIT_EXTERNAL_DIFF' in env:
741 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000742
743 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800744 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000745 else:
746 similarity_options = ['-M%s' % similarity]
747
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000748 try:
749 stdout = sys.stdout.fileno()
750 except AttributeError:
751 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000752 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000753 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000754 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000755 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000756
757
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000758class BuildbucketResponseException(Exception):
759 pass
760
761
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762class Settings(object):
763 def __init__(self):
764 self.default_server = None
765 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000766 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 self.tree_status_url = None
768 self.viewvc_url = None
769 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000770 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000771 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000772 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000774 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000775 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776
777 def LazyUpdateIfNeeded(self):
778 """Updates the settings from a codereview.settings file, if available."""
779 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000780 # The only value that actually changes the behavior is
781 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000782 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000783 error_ok=True
784 ).strip().lower()
785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000787 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 LoadCodereviewSettingsFromFile(cr_settings_file)
789 self.updated = True
790
791 def GetDefaultServerUrl(self, error_ok=False):
792 if not self.default_server:
793 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000794 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 if error_ok:
797 return self.default_server
798 if not self.default_server:
799 error_message = ('Could not find settings file. You must configure '
800 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000801 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000802 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 return self.default_server
804
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000805 @staticmethod
806 def GetRelativeRoot():
807 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000810 if self.root is None:
811 self.root = os.path.abspath(self.GetRelativeRoot())
812 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000814 def GetGitMirror(self, remote='origin'):
815 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000816 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000817 if not os.path.isdir(local_url):
818 return None
819 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
820 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100821 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100822 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000823 if mirror.exists():
824 return mirror
825 return None
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 def GetTreeStatusUrl(self, error_ok=False):
828 if not self.tree_status_url:
829 error_message = ('You must configure your tree status URL by running '
830 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000831 self.tree_status_url = self._GetRietveldConfig(
832 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 return self.tree_status_url
834
835 def GetViewVCUrl(self):
836 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000837 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 return self.viewvc_url
839
Mark Mentovai57c47212017-03-09 11:14:09 -0500840 def GetBugLineFormat(self):
841 # rietveld.bug-line-format should have a %s where the list of bugs should
842 # go. This is a bit of a quirk, because normal people will always want the
843 # bug list to go right after a prefix like BUG= or Bug:. The %s format
844 # approach is used strictly because there isn't a great way to carry the
845 # desired space after Bug: all the way from codereview.settings to here
846 # without treating : specially or inventing a quoting scheme.
847 bug_line_format = self._GetRietveldConfig('bug-line-format', error_ok=True)
848 if not bug_line_format:
849 # TODO(tandrii): change this to 'Bug: %s' to be a proper Gerrit footer.
850 bug_line_format = 'BUG=%s'
851 return bug_line_format
852
rmistry@google.com90752582014-01-14 21:04:50 +0000853 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000854 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000855
rmistry@google.com78948ed2015-07-08 23:09:57 +0000856 def GetIsSkipDependencyUpload(self, branch_name):
857 """Returns true if specified branch should skip dep uploads."""
858 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
859 error_ok=True)
860
rmistry@google.com5626a922015-02-26 14:03:30 +0000861 def GetRunPostUploadHook(self):
862 run_post_upload_hook = self._GetRietveldConfig(
863 'run-post-upload-hook', error_ok=True)
864 return run_post_upload_hook == "True"
865
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000866 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000867 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000868
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000869 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000870 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000871
ukai@chromium.orge8077812012-02-03 03:41:46 +0000872 def GetIsGerrit(self):
873 """Return true if this repo is assosiated with gerrit code review system."""
874 if self.is_gerrit is None:
875 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
876 return self.is_gerrit
877
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000878 def GetSquashGerritUploads(self):
879 """Return true if uploads to Gerrit should be squashed by default."""
880 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700881 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
882 if self.squash_gerrit_uploads is None:
883 # Default is squash now (http://crbug.com/611892#c23).
884 self.squash_gerrit_uploads = not (
885 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
886 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000887 return self.squash_gerrit_uploads
888
tandriia60502f2016-06-20 02:01:53 -0700889 def GetSquashGerritUploadsOverride(self):
890 """Return True or False if codereview.settings should be overridden.
891
892 Returns None if no override has been defined.
893 """
894 # See also http://crbug.com/611892#c23
895 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
896 error_ok=True).strip()
897 if result == 'true':
898 return True
899 if result == 'false':
900 return False
901 return None
902
tandrii@chromium.org28253532016-04-14 13:46:56 +0000903 def GetGerritSkipEnsureAuthenticated(self):
904 """Return True if EnsureAuthenticated should not be done for Gerrit
905 uploads."""
906 if self.gerrit_skip_ensure_authenticated is None:
907 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000908 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000909 error_ok=True).strip() == 'true')
910 return self.gerrit_skip_ensure_authenticated
911
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000912 def GetGitEditor(self):
913 """Return the editor specified in the git config, or None if none is."""
914 if self.git_editor is None:
915 self.git_editor = self._GetConfig('core.editor', error_ok=True)
916 return self.git_editor or None
917
thestig@chromium.org44202a22014-03-11 19:22:18 +0000918 def GetLintRegex(self):
919 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
920 DEFAULT_LINT_REGEX)
921
922 def GetLintIgnoreRegex(self):
923 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
924 DEFAULT_LINT_IGNORE_REGEX)
925
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000926 def GetProject(self):
927 if not self.project:
928 self.project = self._GetRietveldConfig('project', error_ok=True)
929 return self.project
930
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000931 def _GetRietveldConfig(self, param, **kwargs):
932 return self._GetConfig('rietveld.' + param, **kwargs)
933
rmistry@google.com78948ed2015-07-08 23:09:57 +0000934 def _GetBranchConfig(self, branch_name, param, **kwargs):
935 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
936
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 def _GetConfig(self, param, **kwargs):
938 self.LazyUpdateIfNeeded()
939 return RunGit(['config', param], **kwargs).strip()
940
941
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100942@contextlib.contextmanager
943def _get_gerrit_project_config_file(remote_url):
944 """Context manager to fetch and store Gerrit's project.config from
945 refs/meta/config branch and store it in temp file.
946
947 Provides a temporary filename or None if there was error.
948 """
949 error, _ = RunGitWithCode([
950 'fetch', remote_url,
951 '+refs/meta/config:refs/git_cl/meta/config'])
952 if error:
953 # Ref doesn't exist or isn't accessible to current user.
954 print('WARNING: failed to fetch project config for %s: %s' %
955 (remote_url, error))
956 yield None
957 return
958
959 error, project_config_data = RunGitWithCode(
960 ['show', 'refs/git_cl/meta/config:project.config'])
961 if error:
962 print('WARNING: project.config file not found')
963 yield None
964 return
965
966 with gclient_utils.temporary_directory() as tempdir:
967 project_config_file = os.path.join(tempdir, 'project.config')
968 gclient_utils.FileWrite(project_config_file, project_config_data)
969 yield project_config_file
970
971
972def _is_git_numberer_enabled(remote_url, remote_ref):
973 """Returns True if Git Numberer is enabled on this ref."""
974 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100975 KNOWN_PROJECTS_WHITELIST = [
976 'chromium/src',
977 'external/webrtc',
978 'v8/v8',
979 ]
980
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100981 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
982 url_parts = urlparse.urlparse(remote_url)
983 project_name = url_parts.path.lstrip('/').rstrip('git./')
984 for known in KNOWN_PROJECTS_WHITELIST:
985 if project_name.endswith(known):
986 break
987 else:
988 # Early exit to avoid extra fetches for repos that aren't using Git
989 # Numberer.
990 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100991
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100992 with _get_gerrit_project_config_file(remote_url) as project_config_file:
993 if project_config_file is None:
994 # Failed to fetch project.config, which shouldn't happen on open source
995 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100996 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100997 def get_opts(x):
998 code, out = RunGitWithCode(
999 ['config', '-f', project_config_file, '--get-all',
1000 'plugin.git-numberer.validate-%s-refglob' % x])
1001 if code == 0:
1002 return out.strip().splitlines()
1003 return []
1004 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001005
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001006 logging.info('validator config enabled %s disabled %s refglobs for '
1007 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00001008
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001009 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001010 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001011 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001012 return True
1013 return False
1014
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001015 if match_refglobs(disabled):
1016 return False
1017 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001018
1019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020def ShortBranchName(branch):
1021 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001022 return branch.replace('refs/heads/', '', 1)
1023
1024
1025def GetCurrentBranchRef():
1026 """Returns branch ref (e.g., refs/heads/master) or None."""
1027 return RunGit(['symbolic-ref', 'HEAD'],
1028 stderr=subprocess2.VOID, error_ok=True).strip() or None
1029
1030
1031def GetCurrentBranch():
1032 """Returns current branch or None.
1033
1034 For refs/heads/* branches, returns just last part. For others, full ref.
1035 """
1036 branchref = GetCurrentBranchRef()
1037 if branchref:
1038 return ShortBranchName(branchref)
1039 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040
1041
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001042class _CQState(object):
1043 """Enum for states of CL with respect to Commit Queue."""
1044 NONE = 'none'
1045 DRY_RUN = 'dry_run'
1046 COMMIT = 'commit'
1047
1048 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1049
1050
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001051class _ParsedIssueNumberArgument(object):
1052 def __init__(self, issue=None, patchset=None, hostname=None):
1053 self.issue = issue
1054 self.patchset = patchset
1055 self.hostname = hostname
1056
1057 @property
1058 def valid(self):
1059 return self.issue is not None
1060
1061
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001062def ParseIssueNumberArgument(arg):
1063 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1064 fail_result = _ParsedIssueNumberArgument()
1065
1066 if arg.isdigit():
1067 return _ParsedIssueNumberArgument(issue=int(arg))
1068 if not arg.startswith('http'):
1069 return fail_result
1070 url = gclient_utils.UpgradeToHttps(arg)
1071 try:
1072 parsed_url = urlparse.urlparse(url)
1073 except ValueError:
1074 return fail_result
1075 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1076 tmp = cls.ParseIssueURL(parsed_url)
1077 if tmp is not None:
1078 return tmp
1079 return fail_result
1080
1081
Aaron Gablea45ee112016-11-22 15:14:38 -08001082class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001083 def __init__(self, issue, url):
1084 self.issue = issue
1085 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001086 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001087
1088 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001089 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001090 self.issue, self.url)
1091
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001094 """Changelist works with one changelist in local branch.
1095
1096 Supports two codereview backends: Rietveld or Gerrit, selected at object
1097 creation.
1098
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001099 Notes:
1100 * Not safe for concurrent multi-{thread,process} use.
1101 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001102 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001103 """
1104
1105 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1106 """Create a new ChangeList instance.
1107
1108 If issue is given, the codereview must be given too.
1109
1110 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1111 Otherwise, it's decided based on current configuration of the local branch,
1112 with default being 'rietveld' for backwards compatibility.
1113 See _load_codereview_impl for more details.
1114
1115 **kwargs will be passed directly to codereview implementation.
1116 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001118 global settings
1119 if not settings:
1120 # Happens when git_cl.py is used as a utility library.
1121 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001122
1123 if issue:
1124 assert codereview, 'codereview must be known, if issue is known'
1125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 self.branchref = branchref
1127 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001128 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 self.branch = ShortBranchName(self.branchref)
1130 else:
1131 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001133 self.lookedup_issue = False
1134 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 self.has_description = False
1136 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001137 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001139 self.cc = None
1140 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001141 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001142
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001144 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001146 assert self._codereview_impl
1147 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001148
1149 def _load_codereview_impl(self, codereview=None, **kwargs):
1150 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1152 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1153 self._codereview = codereview
1154 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001155 return
1156
1157 # Automatic selection based on issue number set for a current branch.
1158 # Rietveld takes precedence over Gerrit.
1159 assert not self.issue
1160 # Whether we find issue or not, we are doing the lookup.
1161 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001162 if self.GetBranch():
1163 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1164 issue = _git_get_branch_config_value(
1165 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1166 if issue:
1167 self._codereview = codereview
1168 self._codereview_impl = cls(self, **kwargs)
1169 self.issue = int(issue)
1170 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001171
1172 # No issue is set for this branch, so decide based on repo-wide settings.
1173 return self._load_codereview_impl(
1174 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1175 **kwargs)
1176
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001177 def IsGerrit(self):
1178 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001179
1180 def GetCCList(self):
1181 """Return the users cc'd on this CL.
1182
agable92bec4f2016-08-24 09:27:27 -07001183 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001184 """
1185 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001186 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001187 more_cc = ','.join(self.watchers)
1188 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1189 return self.cc
1190
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001191 def GetCCListWithoutDefault(self):
1192 """Return the users cc'd on this CL excluding default ones."""
1193 if self.cc is None:
1194 self.cc = ','.join(self.watchers)
1195 return self.cc
1196
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001197 def SetWatchers(self, watchers):
1198 """Set the list of email addresses that should be cc'd based on the changed
1199 files in this CL.
1200 """
1201 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202
1203 def GetBranch(self):
1204 """Returns the short branch name, e.g. 'master'."""
1205 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001207 if not branchref:
1208 return None
1209 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 self.branch = ShortBranchName(self.branchref)
1211 return self.branch
1212
1213 def GetBranchRef(self):
1214 """Returns the full branch name, e.g. 'refs/heads/master'."""
1215 self.GetBranch() # Poke the lazy loader.
1216 return self.branchref
1217
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001218 def ClearBranch(self):
1219 """Clears cached branch data of this object."""
1220 self.branch = self.branchref = None
1221
tandrii5d48c322016-08-18 16:19:37 -07001222 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1223 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1224 kwargs['branch'] = self.GetBranch()
1225 return _git_get_branch_config_value(key, default, **kwargs)
1226
1227 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1228 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1229 assert self.GetBranch(), (
1230 'this CL must have an associated branch to %sset %s%s' %
1231 ('un' if value is None else '',
1232 key,
1233 '' if value is None else ' to %r' % value))
1234 kwargs['branch'] = self.GetBranch()
1235 return _git_set_branch_config_value(key, value, **kwargs)
1236
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 @staticmethod
1238 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001239 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 e.g. 'origin', 'refs/heads/master'
1241 """
1242 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001243 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001246 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001248 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1249 error_ok=True).strip()
1250 if upstream_branch:
1251 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001253 # Else, try to guess the origin remote.
1254 remote_branches = RunGit(['branch', '-r']).split()
1255 if 'origin/master' in remote_branches:
1256 # Fall back on origin/master if it exits.
1257 remote = 'origin'
1258 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001260 DieWithError(
1261 'Unable to determine default branch to diff against.\n'
1262 'Either pass complete "git diff"-style arguments, like\n'
1263 ' git cl upload origin/master\n'
1264 'or verify this branch is set up to track another \n'
1265 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266
1267 return remote, upstream_branch
1268
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001269 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001270 upstream_branch = self.GetUpstreamBranch()
1271 if not BranchExists(upstream_branch):
1272 DieWithError('The upstream for the current branch (%s) does not exist '
1273 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001274 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001275 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 def GetUpstreamBranch(self):
1278 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001281 upstream_branch = upstream_branch.replace('refs/heads/',
1282 'refs/remotes/%s/' % remote)
1283 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1284 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 self.upstream_branch = upstream_branch
1286 return self.upstream_branch
1287
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001289 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 remote, branch = None, self.GetBranch()
1291 seen_branches = set()
1292 while branch not in seen_branches:
1293 seen_branches.add(branch)
1294 remote, branch = self.FetchUpstreamTuple(branch)
1295 branch = ShortBranchName(branch)
1296 if remote != '.' or branch.startswith('refs/remotes'):
1297 break
1298 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001299 remotes = RunGit(['remote'], error_ok=True).split()
1300 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001302 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001304 logging.warn('Could not determine which remote this change is '
1305 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001306 else:
1307 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001308 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 branch = 'HEAD'
1310 if branch.startswith('refs/remotes'):
1311 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001312 elif branch.startswith('refs/branch-heads/'):
1313 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 else:
1315 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001316 return self._remote
1317
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 def GitSanityChecks(self, upstream_git_obj):
1319 """Checks git repo status and ensures diff is from local commits."""
1320
sbc@chromium.org79706062015-01-14 21:18:12 +00001321 if upstream_git_obj is None:
1322 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001323 print('ERROR: unable to determine current branch (detached HEAD?)',
1324 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001325 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001326 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001327 return False
1328
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 # Verify the commit we're diffing against is in our current branch.
1330 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1331 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1332 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001333 print('ERROR: %s is not in the current branch. You may need to rebase '
1334 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 return False
1336
1337 # List the commits inside the diff, and verify they are all local.
1338 commits_in_diff = RunGit(
1339 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1340 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1341 remote_branch = remote_branch.strip()
1342 if code != 0:
1343 _, remote_branch = self.GetRemoteBranch()
1344
1345 commits_in_remote = RunGit(
1346 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1347
1348 common_commits = set(commits_in_diff) & set(commits_in_remote)
1349 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001350 print('ERROR: Your diff contains %d commits already in %s.\n'
1351 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1352 'the diff. If you are using a custom git flow, you can override'
1353 ' the reference used for this check with "git config '
1354 'gitcl.remotebranch <git-ref>".' % (
1355 len(common_commits), remote_branch, upstream_git_obj),
1356 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001357 return False
1358 return True
1359
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001360 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001361 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001362
1363 Returns None if it is not set.
1364 """
tandrii5d48c322016-08-18 16:19:37 -07001365 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001366
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 def GetRemoteUrl(self):
1368 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1369
1370 Returns None if there is no remote.
1371 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001372 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001373 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1374
1375 # If URL is pointing to a local directory, it is probably a git cache.
1376 if os.path.isdir(url):
1377 url = RunGit(['config', 'remote.%s.url' % remote],
1378 error_ok=True,
1379 cwd=url).strip()
1380 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001382 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001383 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001384 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001385 self.issue = self._GitGetBranchConfigValue(
1386 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001387 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 return self.issue
1389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 def GetIssueURL(self):
1391 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001392 issue = self.GetIssue()
1393 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001394 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001395 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001397 def GetDescription(self, pretty=False, force=False):
1398 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001400 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 self.has_description = True
1402 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001403 # Set width to 72 columns + 2 space indent.
1404 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001406 lines = self.description.splitlines()
1407 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 return self.description
1409
1410 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001411 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001412 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001413 self.patchset = self._GitGetBranchConfigValue(
1414 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001415 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 return self.patchset
1417
1418 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001419 """Set this branch's patchset. If patchset=0, clears the patchset."""
1420 assert self.GetBranch()
1421 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001422 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001423 else:
1424 self.patchset = int(patchset)
1425 self._GitSetBranchConfigValue(
1426 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001428 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001429 """Set this branch's issue. If issue isn't given, clears the issue."""
1430 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001432 issue = int(issue)
1433 self._GitSetBranchConfigValue(
1434 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001435 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001436 codereview_server = self._codereview_impl.GetCodereviewServer()
1437 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001438 self._GitSetBranchConfigValue(
1439 self._codereview_impl.CodereviewServerConfigKey(),
1440 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 else:
tandrii5d48c322016-08-18 16:19:37 -07001442 # Reset all of these just to be clean.
1443 reset_suffixes = [
1444 'last-upload-hash',
1445 self._codereview_impl.IssueConfigKey(),
1446 self._codereview_impl.PatchsetConfigKey(),
1447 self._codereview_impl.CodereviewServerConfigKey(),
1448 ] + self._PostUnsetIssueProperties()
1449 for prop in reset_suffixes:
1450 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001451 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001452 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453
dnjba1b0f32016-09-02 12:37:42 -07001454 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001455 if not self.GitSanityChecks(upstream_branch):
1456 DieWithError('\nGit sanity check failure')
1457
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001459 if not root:
1460 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001461 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001462
1463 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001464 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001465 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001466 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001467 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001468 except subprocess2.CalledProcessError:
1469 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001470 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001471 'This branch probably doesn\'t exist anymore. To reset the\n'
1472 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001473 ' git branch --set-upstream-to origin/master %s\n'
1474 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001475 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001476
maruel@chromium.org52424302012-08-29 15:14:30 +00001477 issue = self.GetIssue()
1478 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001479 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001480 description = self.GetDescription()
1481 else:
1482 # If the change was never uploaded, use the log messages of all commits
1483 # up to the branch point, as git cl upload will prefill the description
1484 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1486 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001487
1488 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001489 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001490 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001491 name,
1492 description,
1493 absroot,
1494 files,
1495 issue,
1496 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001497 author,
1498 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001499
dsansomee2d6fd92016-09-08 00:10:47 -07001500 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001501 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001502 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001503 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001504
1505 def RunHook(self, committing, may_prompt, verbose, change):
1506 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1507 try:
1508 return presubmit_support.DoPresubmitChecks(change, committing,
1509 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1510 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001511 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1512 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001513 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001514 DieWithError(
1515 ('%s\nMaybe your depot_tools is out of date?\n'
1516 'If all fails, contact maruel@') % e)
1517
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001518 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1519 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001520 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1521 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001522 else:
1523 # Assume url.
1524 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1525 urlparse.urlparse(issue_arg))
1526 if not parsed_issue_arg or not parsed_issue_arg.valid:
1527 DieWithError('Failed to parse issue argument "%s". '
1528 'Must be an issue number or a valid URL.' % issue_arg)
1529 return self._codereview_impl.CMDPatchWithParsedIssue(
1530 parsed_issue_arg, reject, nocommit, directory)
1531
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001532 def CMDUpload(self, options, git_diff_args, orig_args):
1533 """Uploads a change to codereview."""
1534 if git_diff_args:
1535 # TODO(ukai): is it ok for gerrit case?
1536 base_branch = git_diff_args[0]
1537 else:
1538 if self.GetBranch() is None:
1539 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1540
1541 # Default to diffing against common ancestor of upstream branch
1542 base_branch = self.GetCommonAncestorWithUpstream()
1543 git_diff_args = [base_branch, 'HEAD']
1544
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001545 # Fast best-effort checks to abort before running potentially
1546 # expensive hooks if uploading is likely to fail anyway. Passing these
1547 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001548 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001549 self._codereview_impl.EnsureCanUploadPatchset()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550
1551 # Apply watchlists on upload.
1552 change = self.GetChange(base_branch, None)
1553 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1554 files = [f.LocalPath() for f in change.AffectedFiles()]
1555 if not options.bypass_watchlists:
1556 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1557
1558 if not options.bypass_hooks:
1559 if options.reviewers or options.tbr_owners:
1560 # Set the reviewer list now so that presubmit checks can access it.
1561 change_description = ChangeDescription(change.FullDescriptionText())
1562 change_description.update_reviewers(options.reviewers,
1563 options.tbr_owners,
1564 change)
1565 change.SetDescriptionText(change_description.description)
1566 hook_results = self.RunHook(committing=False,
1567 may_prompt=not options.force,
1568 verbose=options.verbose,
1569 change=change)
1570 if not hook_results.should_continue():
1571 return 1
1572 if not options.reviewers and hook_results.reviewers:
1573 options.reviewers = hook_results.reviewers.split(',')
1574
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001575 # TODO(tandrii): Checking local patchset against remote patchset is only
1576 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1577 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001578 latest_patchset = self.GetMostRecentPatchset()
1579 local_patchset = self.GetPatchset()
1580 if (latest_patchset and local_patchset and
1581 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001582 print('The last upload made from this repository was patchset #%d but '
1583 'the most recent patchset on the server is #%d.'
1584 % (local_patchset, latest_patchset))
1585 print('Uploading will still work, but if you\'ve uploaded to this '
1586 'issue from another machine or branch the patch you\'re '
1587 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001588 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589
1590 print_stats(options.similarity, options.find_copies, git_diff_args)
1591 ret = self.CMDUploadChange(options, git_diff_args, change)
1592 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001593 if options.use_commit_queue:
1594 self.SetCQState(_CQState.COMMIT)
1595 elif options.cq_dry_run:
1596 self.SetCQState(_CQState.DRY_RUN)
1597
tandrii5d48c322016-08-18 16:19:37 -07001598 _git_set_branch_config_value('last-upload-hash',
1599 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600 # Run post upload hooks, if specified.
1601 if settings.GetRunPostUploadHook():
1602 presubmit_support.DoPostUploadExecuter(
1603 change,
1604 self,
1605 settings.GetRoot(),
1606 options.verbose,
1607 sys.stdout)
1608
1609 # Upload all dependencies if specified.
1610 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001611 print()
1612 print('--dependencies has been specified.')
1613 print('All dependent local branches will be re-uploaded.')
1614 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615 # Remove the dependencies flag from args so that we do not end up in a
1616 # loop.
1617 orig_args.remove('--dependencies')
1618 ret = upload_branch_deps(self, orig_args)
1619 return ret
1620
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001621 def SetCQState(self, new_state):
1622 """Update the CQ state for latest patchset.
1623
1624 Issue must have been already uploaded and known.
1625 """
1626 assert new_state in _CQState.ALL_STATES
1627 assert self.GetIssue()
1628 return self._codereview_impl.SetCQState(new_state)
1629
qyearsley1fdfcb62016-10-24 13:22:03 -07001630 def TriggerDryRun(self):
1631 """Triggers a dry run and prints a warning on failure."""
1632 # TODO(qyearsley): Either re-use this method in CMDset_commit
1633 # and CMDupload, or change CMDtry to trigger dry runs with
1634 # just SetCQState, and catch keyboard interrupt and other
1635 # errors in that method.
1636 try:
1637 self.SetCQState(_CQState.DRY_RUN)
1638 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1639 return 0
1640 except KeyboardInterrupt:
1641 raise
1642 except:
1643 print('WARNING: failed to trigger CQ Dry Run.\n'
1644 'Either:\n'
1645 ' * your project has no CQ\n'
1646 ' * you don\'t have permission to trigger Dry Run\n'
1647 ' * bug in this code (see stack trace below).\n'
1648 'Consider specifying which bots to trigger manually '
1649 'or asking your project owners for permissions '
1650 'or contacting Chrome Infrastructure team at '
1651 'https://www.chromium.org/infra\n\n')
1652 # Still raise exception so that stack trace is printed.
1653 raise
1654
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001655 # Forward methods to codereview specific implementation.
1656
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001657 def AddComment(self, message):
1658 return self._codereview_impl.AddComment(message)
1659
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001660 def CloseIssue(self):
1661 return self._codereview_impl.CloseIssue()
1662
1663 def GetStatus(self):
1664 return self._codereview_impl.GetStatus()
1665
1666 def GetCodereviewServer(self):
1667 return self._codereview_impl.GetCodereviewServer()
1668
tandriide281ae2016-10-12 06:02:30 -07001669 def GetIssueOwner(self):
1670 """Get owner from codereview, which may differ from this checkout."""
1671 return self._codereview_impl.GetIssueOwner()
1672
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 def GetApprovingReviewers(self):
1674 return self._codereview_impl.GetApprovingReviewers()
1675
1676 def GetMostRecentPatchset(self):
1677 return self._codereview_impl.GetMostRecentPatchset()
1678
tandriide281ae2016-10-12 06:02:30 -07001679 def CannotTriggerTryJobReason(self):
1680 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1681 return self._codereview_impl.CannotTriggerTryJobReason()
1682
tandrii8c5a3532016-11-04 07:52:02 -07001683 def GetTryjobProperties(self, patchset=None):
1684 """Returns dictionary of properties to launch tryjob."""
1685 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1686
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 def __getattr__(self, attr):
1688 # This is because lots of untested code accesses Rietveld-specific stuff
1689 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001690 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001691 # Note that child method defines __getattr__ as well, and forwards it here,
1692 # because _RietveldChangelistImpl is not cleaned up yet, and given
1693 # deprecation of Rietveld, it should probably be just removed.
1694 # Until that time, avoid infinite recursion by bypassing __getattr__
1695 # of implementation class.
1696 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697
1698
1699class _ChangelistCodereviewBase(object):
1700 """Abstract base class encapsulating codereview specifics of a changelist."""
1701 def __init__(self, changelist):
1702 self._changelist = changelist # instance of Changelist
1703
1704 def __getattr__(self, attr):
1705 # Forward methods to changelist.
1706 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1707 # _RietveldChangelistImpl to avoid this hack?
1708 return getattr(self._changelist, attr)
1709
1710 def GetStatus(self):
1711 """Apply a rough heuristic to give a simple summary of an issue's review
1712 or CQ status, assuming adherence to a common workflow.
1713
1714 Returns None if no issue for this branch, or specific string keywords.
1715 """
1716 raise NotImplementedError()
1717
1718 def GetCodereviewServer(self):
1719 """Returns server URL without end slash, like "https://codereview.com"."""
1720 raise NotImplementedError()
1721
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001722 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 """Fetches and returns description from the codereview server."""
1724 raise NotImplementedError()
1725
tandrii5d48c322016-08-18 16:19:37 -07001726 @classmethod
1727 def IssueConfigKey(cls):
1728 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 raise NotImplementedError()
1730
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001731 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001732 def PatchsetConfigKey(cls):
1733 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 raise NotImplementedError()
1735
tandrii5d48c322016-08-18 16:19:37 -07001736 @classmethod
1737 def CodereviewServerConfigKey(cls):
1738 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001739 raise NotImplementedError()
1740
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001741 def _PostUnsetIssueProperties(self):
1742 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001743 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001744
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001745 def GetRieveldObjForPresubmit(self):
1746 # This is an unfortunate Rietveld-embeddedness in presubmit.
1747 # For non-Rietveld codereviews, this probably should return a dummy object.
1748 raise NotImplementedError()
1749
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001750 def GetGerritObjForPresubmit(self):
1751 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1752 return None
1753
dsansomee2d6fd92016-09-08 00:10:47 -07001754 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 """Update the description on codereview site."""
1756 raise NotImplementedError()
1757
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001758 def AddComment(self, message):
1759 """Posts a comment to the codereview site."""
1760 raise NotImplementedError()
1761
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762 def CloseIssue(self):
1763 """Closes the issue."""
1764 raise NotImplementedError()
1765
1766 def GetApprovingReviewers(self):
1767 """Returns a list of reviewers approving the change.
1768
1769 Note: not necessarily committers.
1770 """
1771 raise NotImplementedError()
1772
1773 def GetMostRecentPatchset(self):
1774 """Returns the most recent patchset number from the codereview site."""
1775 raise NotImplementedError()
1776
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001777 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1778 directory):
1779 """Fetches and applies the issue.
1780
1781 Arguments:
1782 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1783 reject: if True, reject the failed patch instead of switching to 3-way
1784 merge. Rietveld only.
1785 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1786 only.
1787 directory: switch to directory before applying the patch. Rietveld only.
1788 """
1789 raise NotImplementedError()
1790
1791 @staticmethod
1792 def ParseIssueURL(parsed_url):
1793 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1794 failed."""
1795 raise NotImplementedError()
1796
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001797 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001798 """Best effort check that user is authenticated with codereview server.
1799
1800 Arguments:
1801 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001802 refresh: whether to attempt to refresh credentials. Ignored if not
1803 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001804 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001805 raise NotImplementedError()
1806
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001807 def EnsureCanUploadPatchset(self):
1808 """Best effort check that uploading isn't supposed to fail for predictable
1809 reasons.
1810
1811 This method should raise informative exception if uploading shouldn't
1812 proceed.
1813 """
1814 pass
1815
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001816 def CMDUploadChange(self, options, args, change):
1817 """Uploads a change to codereview."""
1818 raise NotImplementedError()
1819
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001820 def SetCQState(self, new_state):
1821 """Update the CQ state for latest patchset.
1822
1823 Issue must have been already uploaded and known.
1824 """
1825 raise NotImplementedError()
1826
tandriie113dfd2016-10-11 10:20:12 -07001827 def CannotTriggerTryJobReason(self):
1828 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1829 raise NotImplementedError()
1830
tandriide281ae2016-10-12 06:02:30 -07001831 def GetIssueOwner(self):
1832 raise NotImplementedError()
1833
tandrii8c5a3532016-11-04 07:52:02 -07001834 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001835 raise NotImplementedError()
1836
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001837
1838class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001839 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840 super(_RietveldChangelistImpl, self).__init__(changelist)
1841 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001842 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001843 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001845 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001846 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001847 self._props = None
1848 self._rpc_server = None
1849
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001850 def GetCodereviewServer(self):
1851 if not self._rietveld_server:
1852 # If we're on a branch then get the server potentially associated
1853 # with that branch.
1854 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001855 self._rietveld_server = gclient_utils.UpgradeToHttps(
1856 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001857 if not self._rietveld_server:
1858 self._rietveld_server = settings.GetDefaultServerUrl()
1859 return self._rietveld_server
1860
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001861 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862 """Best effort check that user is authenticated with Rietveld server."""
1863 if self._auth_config.use_oauth2:
1864 authenticator = auth.get_authenticator_for_host(
1865 self.GetCodereviewServer(), self._auth_config)
1866 if not authenticator.has_cached_credentials():
1867 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001868 if refresh:
1869 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001870
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001871 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872 issue = self.GetIssue()
1873 assert issue
1874 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001875 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001876 except urllib2.HTTPError as e:
1877 if e.code == 404:
1878 DieWithError(
1879 ('\nWhile fetching the description for issue %d, received a '
1880 '404 (not found)\n'
1881 'error. It is likely that you deleted this '
1882 'issue on the server. If this is the\n'
1883 'case, please run\n\n'
1884 ' git cl issue 0\n\n'
1885 'to clear the association with the deleted issue. Then run '
1886 'this command again.') % issue)
1887 else:
1888 DieWithError(
1889 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1890 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001891 print('Warning: Failed to retrieve CL description due to network '
1892 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001893 return ''
1894
1895 def GetMostRecentPatchset(self):
1896 return self.GetIssueProperties()['patchsets'][-1]
1897
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 def GetIssueProperties(self):
1899 if self._props is None:
1900 issue = self.GetIssue()
1901 if not issue:
1902 self._props = {}
1903 else:
1904 self._props = self.RpcServer().get_issue_properties(issue, True)
1905 return self._props
1906
tandriie113dfd2016-10-11 10:20:12 -07001907 def CannotTriggerTryJobReason(self):
1908 props = self.GetIssueProperties()
1909 if not props:
1910 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1911 if props.get('closed'):
1912 return 'CL %s is closed' % self.GetIssue()
1913 if props.get('private'):
1914 return 'CL %s is private' % self.GetIssue()
1915 return None
1916
tandrii8c5a3532016-11-04 07:52:02 -07001917 def GetTryjobProperties(self, patchset=None):
1918 """Returns dictionary of properties to launch tryjob."""
1919 project = (self.GetIssueProperties() or {}).get('project')
1920 return {
1921 'issue': self.GetIssue(),
1922 'patch_project': project,
1923 'patch_storage': 'rietveld',
1924 'patchset': patchset or self.GetPatchset(),
1925 'rietveld': self.GetCodereviewServer(),
1926 }
1927
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 def GetApprovingReviewers(self):
1929 return get_approving_reviewers(self.GetIssueProperties())
1930
tandriide281ae2016-10-12 06:02:30 -07001931 def GetIssueOwner(self):
1932 return (self.GetIssueProperties() or {}).get('owner_email')
1933
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001934 def AddComment(self, message):
1935 return self.RpcServer().add_comment(self.GetIssue(), message)
1936
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 def GetStatus(self):
1938 """Apply a rough heuristic to give a simple summary of an issue's review
1939 or CQ status, assuming adherence to a common workflow.
1940
1941 Returns None if no issue for this branch, or one of the following keywords:
1942 * 'error' - error from review tool (including deleted issues)
1943 * 'unsent' - not sent for review
1944 * 'waiting' - waiting for review
1945 * 'reply' - waiting for owner to reply to review
1946 * 'lgtm' - LGTM from at least one approved reviewer
1947 * 'commit' - in the commit queue
1948 * 'closed' - closed
1949 """
1950 if not self.GetIssue():
1951 return None
1952
1953 try:
1954 props = self.GetIssueProperties()
1955 except urllib2.HTTPError:
1956 return 'error'
1957
1958 if props.get('closed'):
1959 # Issue is closed.
1960 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001961 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001962 # Issue is in the commit queue.
1963 return 'commit'
1964
1965 try:
1966 reviewers = self.GetApprovingReviewers()
1967 except urllib2.HTTPError:
1968 return 'error'
1969
1970 if reviewers:
1971 # Was LGTM'ed.
1972 return 'lgtm'
1973
1974 messages = props.get('messages') or []
1975
tandrii9d2c7a32016-06-22 03:42:45 -07001976 # Skip CQ messages that don't require owner's action.
1977 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1978 if 'Dry run:' in messages[-1]['text']:
1979 messages.pop()
1980 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1981 # This message always follows prior messages from CQ,
1982 # so skip this too.
1983 messages.pop()
1984 else:
1985 # This is probably a CQ messages warranting user attention.
1986 break
1987
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001988 if not messages:
1989 # No message was sent.
1990 return 'unsent'
1991 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001992 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001993 return 'reply'
1994 return 'waiting'
1995
dsansomee2d6fd92016-09-08 00:10:47 -07001996 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001997 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002000 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002002 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002003 return self.SetFlags({flag: value})
2004
2005 def SetFlags(self, flags):
2006 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002007 """
phajdan.jr68598232016-08-10 03:28:28 -07002008 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002009 try:
tandrii4b233bd2016-07-06 03:50:29 -07002010 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002011 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002012 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002013 if e.code == 404:
2014 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2015 if e.code == 403:
2016 DieWithError(
2017 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002018 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002019 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002021 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022 """Returns an upload.RpcServer() to access this review's rietveld instance.
2023 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002024 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002025 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002026 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002027 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002028 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002030 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002031 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002032 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
tandrii5d48c322016-08-18 16:19:37 -07002034 @classmethod
2035 def PatchsetConfigKey(cls):
2036 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037
tandrii5d48c322016-08-18 16:19:37 -07002038 @classmethod
2039 def CodereviewServerConfigKey(cls):
2040 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002041
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002042 def GetRieveldObjForPresubmit(self):
2043 return self.RpcServer()
2044
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002045 def SetCQState(self, new_state):
2046 props = self.GetIssueProperties()
2047 if props.get('private'):
2048 DieWithError('Cannot set-commit on private issue')
2049
2050 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002051 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002052 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002053 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002054 else:
tandrii4b233bd2016-07-06 03:50:29 -07002055 assert new_state == _CQState.DRY_RUN
2056 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002058 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2059 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 # PatchIssue should never be called with a dirty tree. It is up to the
2061 # caller to check this, but just in case we assert here since the
2062 # consequences of the caller not checking this could be dire.
2063 assert(not git_common.is_dirty_git_tree('apply'))
2064 assert(parsed_issue_arg.valid)
2065 self._changelist.issue = parsed_issue_arg.issue
2066 if parsed_issue_arg.hostname:
2067 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2068
skobes6468b902016-10-24 08:45:10 -07002069 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2070 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2071 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002072 try:
skobes6468b902016-10-24 08:45:10 -07002073 scm_obj.apply_patch(patchset_object)
2074 except Exception as e:
2075 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002076 return 1
2077
2078 # If we had an issue, commit the current state and register the issue.
2079 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002080 self.SetIssue(self.GetIssue())
2081 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002082 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2083 'patch from issue %(i)s at patchset '
2084 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2085 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002086 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002087 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002088 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002089 return 0
2090
2091 @staticmethod
2092 def ParseIssueURL(parsed_url):
2093 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2094 return None
wychen3c1c1722016-08-04 11:46:36 -07002095 # Rietveld patch: https://domain/<number>/#ps<patchset>
2096 match = re.match(r'/(\d+)/$', parsed_url.path)
2097 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2098 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002099 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002100 issue=int(match.group(1)),
2101 patchset=int(match2.group(1)),
2102 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002103 # Typical url: https://domain/<issue_number>[/[other]]
2104 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2105 if match:
skobes6468b902016-10-24 08:45:10 -07002106 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 issue=int(match.group(1)),
2108 hostname=parsed_url.netloc)
2109 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2110 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2111 if match:
skobes6468b902016-10-24 08:45:10 -07002112 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002113 issue=int(match.group(1)),
2114 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002115 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 return None
2117
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002118 def CMDUploadChange(self, options, args, change):
2119 """Upload the patch to Rietveld."""
2120 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2121 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2123 if options.emulate_svn_auto_props:
2124 upload_args.append('--emulate_svn_auto_props')
2125
2126 change_desc = None
2127
2128 if options.email is not None:
2129 upload_args.extend(['--email', options.email])
2130
2131 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002132 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 upload_args.extend(['--title', options.title])
2134 if options.message:
2135 upload_args.extend(['--message', options.message])
2136 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002137 print('This branch is associated with issue %s. '
2138 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002139 else:
nodirca166002016-06-27 10:59:51 -07002140 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002141 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002142 if options.message:
2143 message = options.message
2144 else:
2145 message = CreateDescriptionFromLog(args)
2146 if options.title:
2147 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002148 change_desc = ChangeDescription(message)
2149 if options.reviewers or options.tbr_owners:
2150 change_desc.update_reviewers(options.reviewers,
2151 options.tbr_owners,
2152 change)
2153 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002154 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155
2156 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 return 1
2159
2160 upload_args.extend(['--message', change_desc.description])
2161 if change_desc.get_reviewers():
2162 upload_args.append('--reviewers=%s' % ','.join(
2163 change_desc.get_reviewers()))
2164 if options.send_mail:
2165 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002166 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002167 upload_args.append('--send_mail')
2168
2169 # We check this before applying rietveld.private assuming that in
2170 # rietveld.cc only addresses which we can send private CLs to are listed
2171 # if rietveld.private is set, and so we should ignore rietveld.cc only
2172 # when --private is specified explicitly on the command line.
2173 if options.private:
2174 logging.warn('rietveld.cc is ignored since private flag is specified. '
2175 'You need to review and add them manually if necessary.')
2176 cc = self.GetCCListWithoutDefault()
2177 else:
2178 cc = self.GetCCList()
2179 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002180 if change_desc.get_cced():
2181 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002182 if cc:
2183 upload_args.extend(['--cc', cc])
2184
2185 if options.private or settings.GetDefaultPrivateFlag() == "True":
2186 upload_args.append('--private')
2187
2188 upload_args.extend(['--git_similarity', str(options.similarity)])
2189 if not options.find_copies:
2190 upload_args.extend(['--git_no_find_copies'])
2191
2192 # Include the upstream repo's URL in the change -- this is useful for
2193 # projects that have their source spread across multiple repos.
2194 remote_url = self.GetGitBaseUrlFromConfig()
2195 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002196 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2197 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2198 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002201 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 if target_ref:
2203 upload_args.extend(['--target_ref', target_ref])
2204
2205 # Look for dependent patchsets. See crbug.com/480453 for more details.
2206 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2207 upstream_branch = ShortBranchName(upstream_branch)
2208 if remote is '.':
2209 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002210 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002212 print()
2213 print('Skipping dependency patchset upload because git config '
2214 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2215 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 else:
2217 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002218 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 auth_config=auth_config)
2220 branch_cl_issue_url = branch_cl.GetIssueURL()
2221 branch_cl_issue = branch_cl.GetIssue()
2222 branch_cl_patchset = branch_cl.GetPatchset()
2223 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2224 upload_args.extend(
2225 ['--depends_on_patchset', '%s:%s' % (
2226 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002227 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 '\n'
2229 'The current branch (%s) is tracking a local branch (%s) with '
2230 'an associated CL.\n'
2231 'Adding %s/#ps%s as a dependency patchset.\n'
2232 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2233 branch_cl_patchset))
2234
2235 project = settings.GetProject()
2236 if project:
2237 upload_args.extend(['--project', project])
2238
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 try:
2240 upload_args = ['upload'] + upload_args + args
2241 logging.info('upload.RealMain(%s)', upload_args)
2242 issue, patchset = upload.RealMain(upload_args)
2243 issue = int(issue)
2244 patchset = int(patchset)
2245 except KeyboardInterrupt:
2246 sys.exit(1)
2247 except:
2248 # If we got an exception after the user typed a description for their
2249 # change, back up the description before re-raising.
2250 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002251 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002252 raise
2253
2254 if not self.GetIssue():
2255 self.SetIssue(issue)
2256 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 return 0
2258
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002259
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002261 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262 # auth_config is Rietveld thing, kept here to preserve interface only.
2263 super(_GerritChangelistImpl, self).__init__(changelist)
2264 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002265 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002266 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002267 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002268 # Map from change number (issue) to its detail cache.
2269 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002270
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002271 if codereview_host is not None:
2272 assert not codereview_host.startswith('https://'), codereview_host
2273 self._gerrit_host = codereview_host
2274 self._gerrit_server = 'https://%s' % codereview_host
2275
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002276 def _GetGerritHost(self):
2277 # Lazy load of configs.
2278 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002279 if self._gerrit_host and '.' not in self._gerrit_host:
2280 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2281 # This happens for internal stuff http://crbug.com/614312.
2282 parsed = urlparse.urlparse(self.GetRemoteUrl())
2283 if parsed.scheme == 'sso':
2284 print('WARNING: using non https URLs for remote is likely broken\n'
2285 ' Your current remote is: %s' % self.GetRemoteUrl())
2286 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2287 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002288 return self._gerrit_host
2289
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002290 def _GetGitHost(self):
2291 """Returns git host to be used when uploading change to Gerrit."""
2292 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2293
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002294 def GetCodereviewServer(self):
2295 if not self._gerrit_server:
2296 # If we're on a branch then get the server potentially associated
2297 # with that branch.
2298 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002299 self._gerrit_server = self._GitGetBranchConfigValue(
2300 self.CodereviewServerConfigKey())
2301 if self._gerrit_server:
2302 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002303 if not self._gerrit_server:
2304 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2305 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002306 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307 parts[0] = parts[0] + '-review'
2308 self._gerrit_host = '.'.join(parts)
2309 self._gerrit_server = 'https://%s' % self._gerrit_host
2310 return self._gerrit_server
2311
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002312 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002313 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002314 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002315
tandrii5d48c322016-08-18 16:19:37 -07002316 @classmethod
2317 def PatchsetConfigKey(cls):
2318 return 'gerritpatchset'
2319
2320 @classmethod
2321 def CodereviewServerConfigKey(cls):
2322 return 'gerritserver'
2323
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002324 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002325 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002326 if settings.GetGerritSkipEnsureAuthenticated():
2327 # For projects with unusual authentication schemes.
2328 # See http://crbug.com/603378.
2329 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002330 # Lazy-loader to identify Gerrit and Git hosts.
2331 if gerrit_util.GceAuthenticator.is_gce():
2332 return
2333 self.GetCodereviewServer()
2334 git_host = self._GetGitHost()
2335 assert self._gerrit_server and self._gerrit_host
2336 cookie_auth = gerrit_util.CookiesAuthenticator()
2337
2338 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2339 git_auth = cookie_auth.get_auth_header(git_host)
2340 if gerrit_auth and git_auth:
2341 if gerrit_auth == git_auth:
2342 return
2343 print((
2344 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2345 ' Check your %s or %s file for credentials of hosts:\n'
2346 ' %s\n'
2347 ' %s\n'
2348 ' %s') %
2349 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2350 git_host, self._gerrit_host,
2351 cookie_auth.get_new_password_message(git_host)))
2352 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002353 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002354 return
2355 else:
2356 missing = (
2357 [] if gerrit_auth else [self._gerrit_host] +
2358 [] if git_auth else [git_host])
2359 DieWithError('Credentials for the following hosts are required:\n'
2360 ' %s\n'
2361 'These are read from %s (or legacy %s)\n'
2362 '%s' % (
2363 '\n '.join(missing),
2364 cookie_auth.get_gitcookies_path(),
2365 cookie_auth.get_netrc_path(),
2366 cookie_auth.get_new_password_message(git_host)))
2367
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002368 def EnsureCanUploadPatchset(self):
2369 """Best effort check that uploading isn't supposed to fail for predictable
2370 reasons.
2371
2372 This method should raise informative exception if uploading shouldn't
2373 proceed.
2374 """
2375 if not self.GetIssue():
2376 return
2377
2378 # Warm change details cache now to avoid RPCs later, reducing latency for
2379 # developers.
2380 self.FetchDescription()
2381
2382 status = self._GetChangeDetail()['status']
2383 if status in ('MERGED', 'ABANDONED'):
2384 DieWithError('Change %s has been %s, new uploads are not allowed' %
2385 (self.GetIssueURL(),
2386 'submitted' if status == 'MERGED' else 'abandoned'))
2387
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002388 def _PostUnsetIssueProperties(self):
2389 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002390 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002391
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392 def GetRieveldObjForPresubmit(self):
2393 class ThisIsNotRietveldIssue(object):
2394 def __nonzero__(self):
2395 # This is a hack to make presubmit_support think that rietveld is not
2396 # defined, yet still ensure that calls directly result in a decent
2397 # exception message below.
2398 return False
2399
2400 def __getattr__(self, attr):
2401 print(
2402 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2403 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2404 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2405 'or use Rietveld for codereview.\n'
2406 'See also http://crbug.com/579160.' % attr)
2407 raise NotImplementedError()
2408 return ThisIsNotRietveldIssue()
2409
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002410 def GetGerritObjForPresubmit(self):
2411 return presubmit_support.GerritAccessor(self._GetGerritHost())
2412
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002413 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002414 """Apply a rough heuristic to give a simple summary of an issue's review
2415 or CQ status, assuming adherence to a common workflow.
2416
2417 Returns None if no issue for this branch, or one of the following keywords:
2418 * 'error' - error from review tool (including deleted issues)
2419 * 'unsent' - no reviewers added
2420 * 'waiting' - waiting for review
2421 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002422 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002423 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424 * 'commit' - in the commit queue
2425 * 'closed' - abandoned
2426 """
2427 if not self.GetIssue():
2428 return None
2429
2430 try:
2431 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002432 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433 return 'error'
2434
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002435 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002436 return 'closed'
2437
2438 cq_label = data['labels'].get('Commit-Queue', {})
2439 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002440 votes = cq_label.get('all', [])
2441 highest_vote = 0
2442 for v in votes:
2443 highest_vote = max(highest_vote, v.get('value', 0))
2444 vote_value = str(highest_vote)
2445 if vote_value != '0':
2446 # Add a '+' if the value is not 0 to match the values in the label.
2447 # The cq_label does not have negatives.
2448 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002449 vote_text = cq_label.get('values', {}).get(vote_value, '')
2450 if vote_text.lower() == 'commit':
2451 return 'commit'
2452
2453 lgtm_label = data['labels'].get('Code-Review', {})
2454 if lgtm_label:
2455 if 'rejected' in lgtm_label:
2456 return 'not lgtm'
2457 if 'approved' in lgtm_label:
2458 return 'lgtm'
2459
2460 if not data.get('reviewers', {}).get('REVIEWER', []):
2461 return 'unsent'
2462
2463 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002464 owner = data['owner'].get('_account_id')
2465 while messages:
2466 last_message_author = messages.pop().get('author', {})
2467 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2468 # Ignore replies from CQ.
2469 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002470 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002471 # Some reply from non-owner.
2472 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002473 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002474
2475 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002476 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002477 return data['revisions'][data['current_revision']]['_number']
2478
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002479 def FetchDescription(self, force=False):
2480 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2481 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002482 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002483 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002484
dsansomee2d6fd92016-09-08 00:10:47 -07002485 def UpdateDescriptionRemote(self, description, force=False):
2486 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2487 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002488 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002489 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002490 'unpublished edit. Either publish the edit in the Gerrit web UI '
2491 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002492
2493 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2494 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002495 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002496 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002497
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002498 def AddComment(self, message):
2499 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2500 msg=message)
2501
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002502 def CloseIssue(self):
2503 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2504
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002505 def GetApprovingReviewers(self):
2506 """Returns a list of reviewers approving the change.
2507
2508 Note: not necessarily committers.
2509 """
2510 raise NotImplementedError()
2511
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002512 def SubmitIssue(self, wait_for_merge=True):
2513 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2514 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002516 def _GetChangeDetail(self, options=None, issue=None,
2517 no_cache=False):
2518 """Returns details of the issue by querying Gerrit and caching results.
2519
2520 If fresh data is needed, set no_cache=True which will clear cache and
2521 thus new data will be fetched from Gerrit.
2522 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002523 options = options or []
2524 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002525 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002526
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002527 # Optimization to avoid multiple RPCs:
2528 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2529 'CURRENT_COMMIT' not in options):
2530 options.append('CURRENT_COMMIT')
2531
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002532 # Normalize issue and options for consistent keys in cache.
2533 issue = str(issue)
2534 options = [o.upper() for o in options]
2535
2536 # Check in cache first unless no_cache is True.
2537 if no_cache:
2538 self._detail_cache.pop(issue, None)
2539 else:
2540 options_set = frozenset(options)
2541 for cached_options_set, data in self._detail_cache.get(issue, []):
2542 # Assumption: data fetched before with extra options is suitable
2543 # for return for a smaller set of options.
2544 # For example, if we cached data for
2545 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2546 # and request is for options=[CURRENT_REVISION],
2547 # THEN we can return prior cached data.
2548 if options_set.issubset(cached_options_set):
2549 return data
2550
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002551 try:
2552 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2553 options, ignore_404=False)
2554 except gerrit_util.GerritError as e:
2555 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002556 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002557 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002558
2559 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002560 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002561
agable32978d92016-11-01 12:55:02 -07002562 def _GetChangeCommit(self, issue=None):
2563 issue = issue or self.GetIssue()
2564 assert issue, 'issue is required to query Gerrit'
2565 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2566 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002567 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002568 return data
2569
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002570 def CMDLand(self, force, bypass_hooks, verbose):
2571 if git_common.is_dirty_git_tree('land'):
2572 return 1
tandriid60367b2016-06-22 05:25:12 -07002573 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2574 if u'Commit-Queue' in detail.get('labels', {}):
2575 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002576 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2577 'which can test and land changes for you. '
2578 'Are you sure you wish to bypass it?\n',
2579 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002580
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002581 differs = True
tandriic4344b52016-08-29 06:04:54 -07002582 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002583 # Note: git diff outputs nothing if there is no diff.
2584 if not last_upload or RunGit(['diff', last_upload]).strip():
2585 print('WARNING: some changes from local branch haven\'t been uploaded')
2586 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002587 if detail['current_revision'] == last_upload:
2588 differs = False
2589 else:
2590 print('WARNING: local branch contents differ from latest uploaded '
2591 'patchset')
2592 if differs:
2593 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002594 confirm_or_exit(
2595 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2596 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002597 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2598 elif not bypass_hooks:
2599 hook_results = self.RunHook(
2600 committing=True,
2601 may_prompt=not force,
2602 verbose=verbose,
2603 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2604 if not hook_results.should_continue():
2605 return 1
2606
2607 self.SubmitIssue(wait_for_merge=True)
2608 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002609 links = self._GetChangeCommit().get('web_links', [])
2610 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002611 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002612 print('Landed as %s' % link.get('url'))
2613 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002614 return 0
2615
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002616 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2617 directory):
2618 assert not reject
2619 assert not nocommit
2620 assert not directory
2621 assert parsed_issue_arg.valid
2622
2623 self._changelist.issue = parsed_issue_arg.issue
2624
2625 if parsed_issue_arg.hostname:
2626 self._gerrit_host = parsed_issue_arg.hostname
2627 self._gerrit_server = 'https://%s' % self._gerrit_host
2628
tandriic2405f52016-10-10 08:13:15 -07002629 try:
2630 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002631 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002632 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002633
2634 if not parsed_issue_arg.patchset:
2635 # Use current revision by default.
2636 revision_info = detail['revisions'][detail['current_revision']]
2637 patchset = int(revision_info['_number'])
2638 else:
2639 patchset = parsed_issue_arg.patchset
2640 for revision_info in detail['revisions'].itervalues():
2641 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2642 break
2643 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002644 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002645 (parsed_issue_arg.patchset, self.GetIssue()))
2646
2647 fetch_info = revision_info['fetch']['http']
2648 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002649 self.SetIssue(self.GetIssue())
2650 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002651 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002652 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002653 (self.GetIssue(), self.GetPatchset()))
2654 return 0
2655
2656 @staticmethod
2657 def ParseIssueURL(parsed_url):
2658 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2659 return None
2660 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2661 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2662 # Short urls like https://domain/<issue_number> can be used, but don't allow
2663 # specifying the patchset (you'd 404), but we allow that here.
2664 if parsed_url.path == '/':
2665 part = parsed_url.fragment
2666 else:
2667 part = parsed_url.path
2668 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2669 if match:
2670 return _ParsedIssueNumberArgument(
2671 issue=int(match.group(2)),
2672 patchset=int(match.group(4)) if match.group(4) else None,
2673 hostname=parsed_url.netloc)
2674 return None
2675
tandrii16e0b4e2016-06-07 10:34:28 -07002676 def _GerritCommitMsgHookCheck(self, offer_removal):
2677 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2678 if not os.path.exists(hook):
2679 return
2680 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2681 # custom developer made one.
2682 data = gclient_utils.FileRead(hook)
2683 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2684 return
2685 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002686 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002687 'and may interfere with it in subtle ways.\n'
2688 'We recommend you remove the commit-msg hook.')
2689 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002690 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002691 gclient_utils.rm_file_or_tree(hook)
2692 print('Gerrit commit-msg hook removed.')
2693 else:
2694 print('OK, will keep Gerrit commit-msg hook in place.')
2695
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002696 def CMDUploadChange(self, options, args, change):
2697 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002698 if options.squash and options.no_squash:
2699 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002700
2701 if not options.squash and not options.no_squash:
2702 # Load default for user, repo, squash=true, in this order.
2703 options.squash = settings.GetSquashGerritUploads()
2704 elif options.no_squash:
2705 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002706
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002707 # We assume the remote called "origin" is the one we want.
2708 # It is probably not worthwhile to support different workflows.
2709 gerrit_remote = 'origin'
2710
2711 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002712 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002713
Aaron Gableb56ad332017-01-06 15:24:31 -08002714 # This may be None; default fallback value is determined in logic below.
2715 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002716 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002717
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002718 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002719 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 if self.GetIssue():
2721 # Try to get the message from a previous upload.
2722 message = self.GetDescription()
2723 if not message:
2724 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002725 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002726 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002727 if not title:
2728 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2729 title = ask_for_data(
2730 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002731 if title == default_title:
2732 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 change_id = self._GetChangeDetail()['change_id']
2734 while True:
2735 footer_change_ids = git_footers.get_footer_change_id(message)
2736 if footer_change_ids == [change_id]:
2737 break
2738 if not footer_change_ids:
2739 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002740 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 continue
2742 # There is already a valid footer but with different or several ids.
2743 # Doing this automatically is non-trivial as we don't want to lose
2744 # existing other footers, yet we want to append just 1 desired
2745 # Change-Id. Thus, just create a new footer, but let user verify the
2746 # new description.
2747 message = '%s\n\nChange-Id: %s' % (message, change_id)
2748 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002749 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002750 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002751 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002752 'Please, check the proposed correction to the description, '
2753 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2754 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2755 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002756 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 if not options.force:
2758 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002759 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002760 message = change_desc.description
2761 if not message:
2762 DieWithError("Description is empty. Aborting...")
2763 # Continue the while loop.
2764 # Sanity check of this code - we should end up with proper message
2765 # footer.
2766 assert [change_id] == git_footers.get_footer_change_id(message)
2767 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002768 else: # if not self.GetIssue()
2769 if options.message:
2770 message = options.message
2771 else:
2772 message = CreateDescriptionFromLog(args)
2773 if options.title:
2774 message = options.title + '\n\n' + message
2775 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002777 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002778 # On first upload, patchset title is always this string, while
2779 # --title flag gets converted to first line of message.
2780 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002781 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002782 if not change_desc.description:
2783 DieWithError("Description is empty. Aborting...")
2784 message = change_desc.description
2785 change_ids = git_footers.get_footer_change_id(message)
2786 if len(change_ids) > 1:
2787 DieWithError('too many Change-Id footers, at most 1 allowed.')
2788 if not change_ids:
2789 # Generate the Change-Id automatically.
2790 message = git_footers.add_footer_change_id(
2791 message, GenerateGerritChangeId(message))
2792 change_desc.set_description(message)
2793 change_ids = git_footers.get_footer_change_id(message)
2794 assert len(change_ids) == 1
2795 change_id = change_ids[0]
2796
2797 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2798 if remote is '.':
2799 # If our upstream branch is local, we base our squashed commit on its
2800 # squashed version.
2801 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2802 # Check the squashed hash of the parent.
2803 parent = RunGit(['config',
2804 'branch.%s.gerritsquashhash' % upstream_branch_name],
2805 error_ok=True).strip()
2806 # Verify that the upstream branch has been uploaded too, otherwise
2807 # Gerrit will create additional CLs when uploading.
2808 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2809 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002810 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002811 '\nUpload upstream branch %s first.\n'
2812 'It is likely that this branch has been rebased since its last '
2813 'upload, so you just need to upload it again.\n'
2814 '(If you uploaded it with --no-squash, then branch dependencies '
2815 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002816 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002817 else:
2818 parent = self.GetCommonAncestorWithUpstream()
2819
2820 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2821 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2822 '-m', message]).strip()
2823 else:
2824 change_desc = ChangeDescription(
2825 options.message or CreateDescriptionFromLog(args))
2826 if not change_desc.description:
2827 DieWithError("Description is empty. Aborting...")
2828
2829 if not git_footers.get_footer_change_id(change_desc.description):
2830 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002831 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2832 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833 ref_to_push = 'HEAD'
2834 parent = '%s/%s' % (gerrit_remote, branch)
2835 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2836
2837 assert change_desc
2838 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2839 ref_to_push)]).splitlines()
2840 if len(commits) > 1:
2841 print('WARNING: This will upload %d commits. Run the following command '
2842 'to see which commits will be uploaded: ' % len(commits))
2843 print('git log %s..%s' % (parent, ref_to_push))
2844 print('You can also use `git squash-branch` to squash these into a '
2845 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002846 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002847
2848 if options.reviewers or options.tbr_owners:
2849 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2850 change)
2851
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002852 # Extra options that can be specified at push time. Doc:
2853 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2854 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002855 if change_desc.get_reviewers(tbr_only=True):
2856 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2857 refspec_opts.append('l=Code-Review+1')
2858
Aaron Gable9b713dd2016-12-14 16:04:21 -08002859 if title:
2860 if not re.match(r'^[\w ]+$', title):
2861 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002862 if not automatic_title:
2863 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002864 'and spaces. You can edit it in the UI. '
2865 'See https://crbug.com/663787.\n'
2866 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002867 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2868 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002869 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002870
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002871 if options.send_mail:
2872 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002873 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002874 refspec_opts.append('notify=ALL')
2875 else:
2876 refspec_opts.append('notify=NONE')
2877
tandrii99a72f22016-08-17 14:33:24 -07002878 reviewers = change_desc.get_reviewers()
2879 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002880 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2881 # side for real (b/34702620).
2882 def clean_invisible_chars(email):
2883 return email.decode('unicode_escape').encode('ascii', 'ignore')
2884 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2885 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002886
agablec6787972016-09-09 16:13:34 -07002887 if options.private:
2888 refspec_opts.append('draft')
2889
rmistry9eadede2016-09-19 11:22:43 -07002890 if options.topic:
2891 # Documentation on Gerrit topics is here:
2892 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2893 refspec_opts.append('topic=%s' % options.topic)
2894
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002895 refspec_suffix = ''
2896 if refspec_opts:
2897 refspec_suffix = '%' + ','.join(refspec_opts)
2898 assert ' ' not in refspec_suffix, (
2899 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002900 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002901
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002902 try:
2903 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002904 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002905 print_stdout=True,
2906 # Flush after every line: useful for seeing progress when running as
2907 # recipe.
2908 filter_fn=lambda _: sys.stdout.flush())
2909 except subprocess2.CalledProcessError:
2910 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002911 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002912
2913 if options.squash:
2914 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2915 change_numbers = [m.group(1)
2916 for m in map(regex.match, push_stdout.splitlines())
2917 if m]
2918 if len(change_numbers) != 1:
2919 DieWithError(
2920 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002921 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002922 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002923 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002924
2925 # Add cc's from the CC_LIST and --cc flag (if any).
2926 cc = self.GetCCList().split(',')
2927 if options.cc:
2928 cc.extend(options.cc)
2929 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002930 if change_desc.get_cced():
2931 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002932 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002933 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002934 self._GetGerritHost(), self.GetIssue(), cc,
2935 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002936 return 0
2937
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002938 def _AddChangeIdToCommitMessage(self, options, args):
2939 """Re-commits using the current message, assumes the commit hook is in
2940 place.
2941 """
2942 log_desc = options.message or CreateDescriptionFromLog(args)
2943 git_command = ['commit', '--amend', '-m', log_desc]
2944 RunGit(git_command)
2945 new_log_desc = CreateDescriptionFromLog(args)
2946 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002947 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002948 return new_log_desc
2949 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002950 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002951
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002952 def SetCQState(self, new_state):
2953 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002954 vote_map = {
2955 _CQState.NONE: 0,
2956 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002957 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002958 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002959 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2960 if new_state == _CQState.DRY_RUN:
2961 # Don't spam everybody reviewer/owner.
2962 kwargs['notify'] = 'NONE'
2963 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002964
tandriie113dfd2016-10-11 10:20:12 -07002965 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002966 try:
2967 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002968 except GerritChangeNotExists:
2969 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002970
2971 if data['status'] in ('ABANDONED', 'MERGED'):
2972 return 'CL %s is closed' % self.GetIssue()
2973
2974 def GetTryjobProperties(self, patchset=None):
2975 """Returns dictionary of properties to launch tryjob."""
2976 data = self._GetChangeDetail(['ALL_REVISIONS'])
2977 patchset = int(patchset or self.GetPatchset())
2978 assert patchset
2979 revision_data = None # Pylint wants it to be defined.
2980 for revision_data in data['revisions'].itervalues():
2981 if int(revision_data['_number']) == patchset:
2982 break
2983 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002984 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002985 (patchset, self.GetIssue()))
2986 return {
2987 'patch_issue': self.GetIssue(),
2988 'patch_set': patchset or self.GetPatchset(),
2989 'patch_project': data['project'],
2990 'patch_storage': 'gerrit',
2991 'patch_ref': revision_data['fetch']['http']['ref'],
2992 'patch_repository_url': revision_data['fetch']['http']['url'],
2993 'patch_gerrit_url': self.GetCodereviewServer(),
2994 }
tandriie113dfd2016-10-11 10:20:12 -07002995
tandriide281ae2016-10-12 06:02:30 -07002996 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002997 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002998
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002999
3000_CODEREVIEW_IMPLEMENTATIONS = {
3001 'rietveld': _RietveldChangelistImpl,
3002 'gerrit': _GerritChangelistImpl,
3003}
3004
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003005
iannuccie53c9352016-08-17 14:40:40 -07003006def _add_codereview_issue_select_options(parser, extra=""):
3007 _add_codereview_select_options(parser)
3008
3009 text = ('Operate on this issue number instead of the current branch\'s '
3010 'implicit issue.')
3011 if extra:
3012 text += ' '+extra
3013 parser.add_option('-i', '--issue', type=int, help=text)
3014
3015
3016def _process_codereview_issue_select_options(parser, options):
3017 _process_codereview_select_options(parser, options)
3018 if options.issue is not None and not options.forced_codereview:
3019 parser.error('--issue must be specified with either --rietveld or --gerrit')
3020
3021
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003022def _add_codereview_select_options(parser):
3023 """Appends --gerrit and --rietveld options to force specific codereview."""
3024 parser.codereview_group = optparse.OptionGroup(
3025 parser, 'EXPERIMENTAL! Codereview override options')
3026 parser.add_option_group(parser.codereview_group)
3027 parser.codereview_group.add_option(
3028 '--gerrit', action='store_true',
3029 help='Force the use of Gerrit for codereview')
3030 parser.codereview_group.add_option(
3031 '--rietveld', action='store_true',
3032 help='Force the use of Rietveld for codereview')
3033
3034
3035def _process_codereview_select_options(parser, options):
3036 if options.gerrit and options.rietveld:
3037 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3038 options.forced_codereview = None
3039 if options.gerrit:
3040 options.forced_codereview = 'gerrit'
3041 elif options.rietveld:
3042 options.forced_codereview = 'rietveld'
3043
3044
tandriif9aefb72016-07-01 09:06:51 -07003045def _get_bug_line_values(default_project, bugs):
3046 """Given default_project and comma separated list of bugs, yields bug line
3047 values.
3048
3049 Each bug can be either:
3050 * a number, which is combined with default_project
3051 * string, which is left as is.
3052
3053 This function may produce more than one line, because bugdroid expects one
3054 project per line.
3055
3056 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3057 ['v8:123', 'chromium:789']
3058 """
3059 default_bugs = []
3060 others = []
3061 for bug in bugs.split(','):
3062 bug = bug.strip()
3063 if bug:
3064 try:
3065 default_bugs.append(int(bug))
3066 except ValueError:
3067 others.append(bug)
3068
3069 if default_bugs:
3070 default_bugs = ','.join(map(str, default_bugs))
3071 if default_project:
3072 yield '%s:%s' % (default_project, default_bugs)
3073 else:
3074 yield default_bugs
3075 for other in sorted(others):
3076 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3077 yield other
3078
3079
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003080class ChangeDescription(object):
3081 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003082 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003083 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Mark Mentovai600d3092017-03-08 12:58:18 -05003084 BUG_LINE = r'^[ \t]*(BUGS?|Bugs?)[ \t]*[:=][ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003085 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003086
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003087 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003089
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003091 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 return '\n'.join(self._description_lines)
3093
3094 def set_description(self, desc):
3095 if isinstance(desc, basestring):
3096 lines = desc.splitlines()
3097 else:
3098 lines = [line.rstrip() for line in desc]
3099 while lines and not lines[0]:
3100 lines.pop(0)
3101 while lines and not lines[-1]:
3102 lines.pop(-1)
3103 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003104
piman@chromium.org336f9122014-09-04 02:16:55 +00003105 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003106 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003107 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003108 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003109 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003110 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003111
agable@chromium.org42c20792013-09-12 17:34:49 +00003112 # Get the set of R= and TBR= lines and remove them from the desciption.
3113 regexp = re.compile(self.R_LINE)
3114 matches = [regexp.match(line) for line in self._description_lines]
3115 new_desc = [l for i, l in enumerate(self._description_lines)
3116 if not matches[i]]
3117 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003118
agable@chromium.org42c20792013-09-12 17:34:49 +00003119 # Construct new unified R= and TBR= lines.
3120 r_names = []
3121 tbr_names = []
3122 for match in matches:
3123 if not match:
3124 continue
3125 people = cleanup_list([match.group(2).strip()])
3126 if match.group(1) == 'TBR':
3127 tbr_names.extend(people)
3128 else:
3129 r_names.extend(people)
3130 for name in r_names:
3131 if name not in reviewers:
3132 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003133 if add_owners_tbr:
3134 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003135 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003136 all_reviewers = set(tbr_names + reviewers)
3137 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3138 all_reviewers)
3139 tbr_names.extend(owners_db.reviewers_for(missing_files,
3140 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003141 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3142 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3143
3144 # Put the new lines in the description where the old first R= line was.
3145 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3146 if 0 <= line_loc < len(self._description_lines):
3147 if new_tbr_line:
3148 self._description_lines.insert(line_loc, new_tbr_line)
3149 if new_r_line:
3150 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003152 if new_r_line:
3153 self.append_footer(new_r_line)
3154 if new_tbr_line:
3155 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003156
tandriif9aefb72016-07-01 09:06:51 -07003157 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003158 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003159 self.set_description([
3160 '# Enter a description of the change.',
3161 '# This will be displayed on the codereview site.',
3162 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003163 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003164 '--------------------',
3165 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003166
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 regexp = re.compile(self.BUG_LINE)
3168 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003169 prefix = settings.GetBugPrefix()
3170 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Mark Mentovai57c47212017-03-09 11:14:09 -05003171 bug_line_format = settings.GetBugLineFormat()
tandriif9aefb72016-07-01 09:06:51 -07003172 for value in values:
Mark Mentovai57c47212017-03-09 11:14:09 -05003173 self.append_footer(bug_line_format % value)
tandriif9aefb72016-07-01 09:06:51 -07003174
agable@chromium.org42c20792013-09-12 17:34:49 +00003175 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003176 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003177 if not content:
3178 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003179 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003180
3181 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003182 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3183 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003184 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003185 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003186
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003187 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003188 """Adds a footer line to the description.
3189
3190 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3191 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3192 that Gerrit footers are always at the end.
3193 """
3194 parsed_footer_line = git_footers.parse_footer(line)
3195 if parsed_footer_line:
3196 # Line is a gerrit footer in the form: Footer-Key: any value.
3197 # Thus, must be appended observing Gerrit footer rules.
3198 self.set_description(
3199 git_footers.add_footer(self.description,
3200 key=parsed_footer_line[0],
3201 value=parsed_footer_line[1]))
3202 return
3203
3204 if not self._description_lines:
3205 self._description_lines.append(line)
3206 return
3207
3208 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3209 if gerrit_footers:
3210 # git_footers.split_footers ensures that there is an empty line before
3211 # actual (gerrit) footers, if any. We have to keep it that way.
3212 assert top_lines and top_lines[-1] == ''
3213 top_lines, separator = top_lines[:-1], top_lines[-1:]
3214 else:
3215 separator = [] # No need for separator if there are no gerrit_footers.
3216
3217 prev_line = top_lines[-1] if top_lines else ''
3218 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3219 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3220 top_lines.append('')
3221 top_lines.append(line)
3222 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003223
tandrii99a72f22016-08-17 14:33:24 -07003224 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003225 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003226 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003227 reviewers = [match.group(2).strip()
3228 for match in matches
3229 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003230 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003231
bradnelsond975b302016-10-23 12:20:23 -07003232 def get_cced(self):
3233 """Retrieves the list of reviewers."""
3234 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3235 cced = [match.group(2).strip() for match in matches if match]
3236 return cleanup_list(cced)
3237
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003238 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3239 """Updates this commit description given the parent.
3240
3241 This is essentially what Gnumbd used to do.
3242 Consult https://goo.gl/WMmpDe for more details.
3243 """
3244 assert parent_msg # No, orphan branch creation isn't supported.
3245 assert parent_hash
3246 assert dest_ref
3247 parent_footer_map = git_footers.parse_footers(parent_msg)
3248 # This will also happily parse svn-position, which GnumbD is no longer
3249 # supporting. While we'd generate correct footers, the verifier plugin
3250 # installed in Gerrit will block such commit (ie git push below will fail).
3251 parent_position = git_footers.get_position(parent_footer_map)
3252
3253 # Cherry-picks may have last line obscuring their prior footers,
3254 # from git_footers perspective. This is also what Gnumbd did.
3255 cp_line = None
3256 if (self._description_lines and
3257 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3258 cp_line = self._description_lines.pop()
3259
3260 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3261
3262 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3263 # user interference with actual footers we'd insert below.
3264 for i, (k, v) in enumerate(parsed_footers):
3265 if k.startswith('Cr-'):
3266 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3267
3268 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003269 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003270 if parent_position[0] == dest_ref:
3271 # Same branch as parent.
3272 number = int(parent_position[1]) + 1
3273 else:
3274 number = 1 # New branch, and extra lineage.
3275 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3276 int(parent_position[1])))
3277
3278 parsed_footers.append(('Cr-Commit-Position',
3279 '%s@{#%d}' % (dest_ref, number)))
3280 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3281
3282 self._description_lines = top_lines
3283 if cp_line:
3284 self._description_lines.append(cp_line)
3285 if self._description_lines[-1] != '':
3286 self._description_lines.append('') # Ensure footer separator.
3287 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3288
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003289
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003290def get_approving_reviewers(props):
3291 """Retrieves the reviewers that approved a CL from the issue properties with
3292 messages.
3293
3294 Note that the list may contain reviewers that are not committer, thus are not
3295 considered by the CQ.
3296 """
3297 return sorted(
3298 set(
3299 message['sender']
3300 for message in props['messages']
3301 if message['approval'] and message['sender'] in props['reviewers']
3302 )
3303 )
3304
3305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003306def FindCodereviewSettingsFile(filename='codereview.settings'):
3307 """Finds the given file starting in the cwd and going up.
3308
3309 Only looks up to the top of the repository unless an
3310 'inherit-review-settings-ok' file exists in the root of the repository.
3311 """
3312 inherit_ok_file = 'inherit-review-settings-ok'
3313 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003314 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003315 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3316 root = '/'
3317 while True:
3318 if filename in os.listdir(cwd):
3319 if os.path.isfile(os.path.join(cwd, filename)):
3320 return open(os.path.join(cwd, filename))
3321 if cwd == root:
3322 break
3323 cwd = os.path.dirname(cwd)
3324
3325
3326def LoadCodereviewSettingsFromFile(fileobj):
3327 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003328 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330 def SetProperty(name, setting, unset_error_ok=False):
3331 fullname = 'rietveld.' + name
3332 if setting in keyvals:
3333 RunGit(['config', fullname, keyvals[setting]])
3334 else:
3335 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3336
tandrii48df5812016-10-17 03:55:37 -07003337 if not keyvals.get('GERRIT_HOST', False):
3338 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339 # Only server setting is required. Other settings can be absent.
3340 # In that case, we ignore errors raised during option deletion attempt.
3341 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003342 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003343 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3344 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
Mark Mentovai57c47212017-03-09 11:14:09 -05003345 SetProperty('bug-line-format', 'BUG_LINE_FORMAT', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003346 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003347 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3348 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003349 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003350 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3351 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003352
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003353 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003354 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003355
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003356 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003357 RunGit(['config', 'gerrit.squash-uploads',
3358 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003359
tandrii@chromium.org28253532016-04-14 13:46:56 +00003360 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003361 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003362 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3363
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003364 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003365 # should be of the form
3366 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3367 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003368 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3369 keyvals['ORIGIN_URL_CONFIG']])
3370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003372def urlretrieve(source, destination):
3373 """urllib is broken for SSL connections via a proxy therefore we
3374 can't use urllib.urlretrieve()."""
3375 with open(destination, 'w') as f:
3376 f.write(urllib2.urlopen(source).read())
3377
3378
ukai@chromium.org712d6102013-11-27 00:52:58 +00003379def hasSheBang(fname):
3380 """Checks fname is a #! script."""
3381 with open(fname) as f:
3382 return f.read(2).startswith('#!')
3383
3384
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003385# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3386def DownloadHooks(*args, **kwargs):
3387 pass
3388
3389
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003390def DownloadGerritHook(force):
3391 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003392
3393 Args:
3394 force: True to update hooks. False to install hooks if not present.
3395 """
3396 if not settings.GetIsGerrit():
3397 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003398 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003399 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3400 if not os.access(dst, os.X_OK):
3401 if os.path.exists(dst):
3402 if not force:
3403 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003404 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003405 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003406 if not hasSheBang(dst):
3407 DieWithError('Not a script: %s\n'
3408 'You need to download from\n%s\n'
3409 'into .git/hooks/commit-msg and '
3410 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003411 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3412 except Exception:
3413 if os.path.exists(dst):
3414 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003415 DieWithError('\nFailed to download hooks.\n'
3416 'You need to download from\n%s\n'
3417 'into .git/hooks/commit-msg and '
3418 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003419
3420
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003421def GetRietveldCodereviewSettingsInteractively():
3422 """Prompt the user for settings."""
3423 server = settings.GetDefaultServerUrl(error_ok=True)
3424 prompt = 'Rietveld server (host[:port])'
3425 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3426 newserver = ask_for_data(prompt + ':')
3427 if not server and not newserver:
3428 newserver = DEFAULT_SERVER
3429 if newserver:
3430 newserver = gclient_utils.UpgradeToHttps(newserver)
3431 if newserver != server:
3432 RunGit(['config', 'rietveld.server', newserver])
3433
3434 def SetProperty(initial, caption, name, is_url):
3435 prompt = caption
3436 if initial:
3437 prompt += ' ("x" to clear) [%s]' % initial
3438 new_val = ask_for_data(prompt + ':')
3439 if new_val == 'x':
3440 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3441 elif new_val:
3442 if is_url:
3443 new_val = gclient_utils.UpgradeToHttps(new_val)
3444 if new_val != initial:
3445 RunGit(['config', 'rietveld.' + name, new_val])
3446
3447 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3448 SetProperty(settings.GetDefaultPrivateFlag(),
3449 'Private flag (rietveld only)', 'private', False)
3450 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3451 'tree-status-url', False)
3452 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3453 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3454 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3455 'run-post-upload-hook', False)
3456
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003457
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003458class _GitCookiesChecker(object):
3459 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003460
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003461 _GOOGLESOURCE = 'googlesource.com'
3462
3463 def __init__(self):
3464 # Cached list of [host, identity, source], where source is either
3465 # .gitcookies or .netrc.
3466 self._all_hosts = None
3467
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003468 def ensure_configured_gitcookies(self):
3469 """Runs checks and suggests fixes to make git use .gitcookies from default
3470 path."""
3471 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3472 configured_path = RunGitSilent(
3473 ['config', '--global', 'http.cookiefile']).strip()
3474 if configured_path:
3475 self._ensure_default_gitcookies_path(configured_path, default)
3476 else:
3477 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003478
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003479 @staticmethod
3480 def _ensure_default_gitcookies_path(configured_path, default_path):
3481 assert configured_path
3482 if configured_path == default_path:
3483 print('git is already configured to use your .gitcookies from %s' %
3484 configured_path)
3485 return
3486
3487 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3488 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3489 (configured_path, default_path))
3490
3491 if not os.path.exists(configured_path):
3492 print('However, your configured .gitcookies file is missing.')
3493 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3494 action='reconfigure')
3495 RunGit(['config', '--global', 'http.cookiefile', default_path])
3496 return
3497
3498 if os.path.exists(default_path):
3499 print('WARNING: default .gitcookies file already exists %s' %
3500 default_path)
3501 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3502 default_path)
3503
3504 confirm_or_exit('Move existing .gitcookies to default location?',
3505 action='move')
3506 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003507 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003508 print('Moved and reconfigured git to use .gitcookies from %s' %
3509 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003510
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003511 @staticmethod
3512 def _configure_gitcookies_path(default_path):
3513 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3514 if os.path.exists(netrc_path):
3515 print('You seem to be using outdated .netrc for git credentials: %s' %
3516 netrc_path)
3517 print('This tool will guide you through setting up recommended '
3518 '.gitcookies store for git credentials.\n'
3519 '\n'
3520 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3521 ' git config --global --unset http.cookiefile\n'
3522 ' mv %s %s.backup\n\n' % (default_path, default_path))
3523 confirm_or_exit(action='setup .gitcookies')
3524 RunGit(['config', '--global', 'http.cookiefile', default_path])
3525 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003526
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003527 def get_hosts_with_creds(self, include_netrc=False):
3528 if self._all_hosts is None:
3529 a = gerrit_util.CookiesAuthenticator()
3530 self._all_hosts = [
3531 (h, u, s)
3532 for h, u, s in itertools.chain(
3533 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3534 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3535 )
3536 if h.endswith(self._GOOGLESOURCE)
3537 ]
3538
3539 if include_netrc:
3540 return self._all_hosts
3541 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3542
3543 def print_current_creds(self, include_netrc=False):
3544 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3545 if not hosts:
3546 print('No Git/Gerrit credentials found')
3547 return
3548 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3549 header = [('Host', 'User', 'Which file'),
3550 ['=' * l for l in lengths]]
3551 for row in (header + hosts):
3552 print('\t'.join((('%%+%ds' % l) % s)
3553 for l, s in zip(lengths, row)))
3554
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003555 @staticmethod
3556 def _parse_identity(identity):
3557 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3558 username, domain = identity.split('.', 1)
3559 if username.startswith('git-'):
3560 username = username[len('git-'):]
3561 return username, domain
3562
3563 def _get_usernames_of_domain(self, domain):
3564 """Returns list of usernames referenced by .gitcookies in a given domain."""
3565 identities_by_domain = {}
3566 for _, identity, _ in self.get_hosts_with_creds():
3567 username, domain = self._parse_identity(identity)
3568 identities_by_domain.setdefault(domain, []).append(username)
3569 return identities_by_domain.get(domain)
3570
3571 def _canonical_git_googlesource_host(self, host):
3572 """Normalizes Gerrit hosts (with '-review') to Git host."""
3573 assert host.endswith(self._GOOGLESOURCE)
3574 # Prefix doesn't include '.' at the end.
3575 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3576 if prefix.endswith('-review'):
3577 prefix = prefix[:-len('-review')]
3578 return prefix + '.' + self._GOOGLESOURCE
3579
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003580 def _canonical_gerrit_googlesource_host(self, host):
3581 git_host = self._canonical_git_googlesource_host(host)
3582 prefix = git_host.split('.', 1)[0]
3583 return prefix + '-review.' + self._GOOGLESOURCE
3584
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003585 def has_generic_host(self):
3586 """Returns whether generic .googlesource.com has been configured.
3587
3588 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3589 """
3590 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3591 if host == '.' + self._GOOGLESOURCE:
3592 return True
3593 return False
3594
3595 def _get_git_gerrit_identity_pairs(self):
3596 """Returns map from canonic host to pair of identities (Git, Gerrit).
3597
3598 One of identities might be None, meaning not configured.
3599 """
3600 host_to_identity_pairs = {}
3601 for host, identity, _ in self.get_hosts_with_creds():
3602 canonical = self._canonical_git_googlesource_host(host)
3603 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3604 idx = 0 if canonical == host else 1
3605 pair[idx] = identity
3606 return host_to_identity_pairs
3607
3608 def get_partially_configured_hosts(self):
3609 return set(
3610 host for host, identities_pair in
3611 self._get_git_gerrit_identity_pairs().iteritems()
3612 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3613
3614 def get_conflicting_hosts(self):
3615 return set(
3616 host for host, (i1, i2) in
3617 self._get_git_gerrit_identity_pairs().iteritems()
3618 if None not in (i1, i2) and i1 != i2)
3619
3620 def get_duplicated_hosts(self):
3621 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3622 return set(host for host, count in counters.iteritems() if count > 1)
3623
3624 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3625 'chromium.googlesource.com': 'chromium.org',
3626 'chrome-internal.googlesource.com': 'google.com',
3627 }
3628
3629 def get_hosts_with_wrong_identities(self):
3630 """Finds hosts which **likely** reference wrong identities.
3631
3632 Note: skips hosts which have conflicting identities for Git and Gerrit.
3633 """
3634 hosts = set()
3635 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3636 pair = self._get_git_gerrit_identity_pairs().get(host)
3637 if pair and pair[0] == pair[1]:
3638 _, domain = self._parse_identity(pair[0])
3639 if domain != expected:
3640 hosts.add(host)
3641 return hosts
3642
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003643 @staticmethod
3644 def print_hosts(hosts, extra_column_func=None):
3645 hosts = sorted(hosts)
3646 assert hosts
3647 if extra_column_func is None:
3648 extras = [''] * len(hosts)
3649 else:
3650 extras = [extra_column_func(host) for host in hosts]
3651 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3652 for he in zip(hosts, extras):
3653 print(tmpl % he)
3654 print()
3655
3656 def find_and_report_problems(self):
3657 """Returns True if there was at least one problem, else False."""
3658 problems = [False]
3659 def add_problem():
3660 if not problems[0]:
3661 print('.gitcookies problem report:\n')
3662 problems[0] = True
3663
3664 if self.has_generic_host():
3665 add_problem()
3666 print(' .googlesource.com record detected\n'
3667 ' Chrome Infrastructure team recommends to list full host names '
3668 'explicitly.\n')
3669
3670 dups = self.get_duplicated_hosts()
3671 if dups:
3672 add_problem()
3673 print(' The following hosts were defined twice:\n')
3674 self.print_hosts(dups)
3675
3676 partial = self.get_partially_configured_hosts()
3677 if partial:
3678 add_problem()
3679 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3680 'These hosts are missing:')
3681 self.print_hosts(partial)
3682
3683 conflicting = self.get_conflicting_hosts()
3684 if conflicting:
3685 add_problem()
3686 print(' The following Git hosts have differing credentials from their '
3687 'Gerrit counterparts:\n')
3688 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3689 tuple(self._get_git_gerrit_identity_pairs()[host]))
3690
3691 wrong = self.get_hosts_with_wrong_identities()
3692 if wrong:
3693 add_problem()
3694 print(' These hosts likely use wrong identity:\n')
3695 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3696 (self._get_git_gerrit_identity_pairs()[host][0],
3697 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3698 return problems[0]
3699
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003700
3701def CMDcreds_check(parser, args):
3702 """Checks credentials and suggests changes."""
3703 _, _ = parser.parse_args(args)
3704
3705 if gerrit_util.GceAuthenticator.is_gce():
3706 DieWithError('this command is not designed for GCE, are you on a bot?')
3707
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003708 checker = _GitCookiesChecker()
3709 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003710
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003711 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003712 checker.print_current_creds(include_netrc=True)
3713
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003714 if not checker.find_and_report_problems():
3715 print('\nNo problems detected in your .gitcookies')
3716 return 0
3717 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003718
3719
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003720@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003722 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723
tandrii5d0a0422016-09-14 06:24:35 -07003724 print('WARNING: git cl config works for Rietveld only')
3725 # TODO(tandrii): remove this once we switch to Gerrit.
3726 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003727 parser.add_option('--activate-update', action='store_true',
3728 help='activate auto-updating [rietveld] section in '
3729 '.git/config')
3730 parser.add_option('--deactivate-update', action='store_true',
3731 help='deactivate auto-updating [rietveld] section in '
3732 '.git/config')
3733 options, args = parser.parse_args(args)
3734
3735 if options.deactivate_update:
3736 RunGit(['config', 'rietveld.autoupdate', 'false'])
3737 return
3738
3739 if options.activate_update:
3740 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3741 return
3742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003743 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003744 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003745 return 0
3746
3747 url = args[0]
3748 if not url.endswith('codereview.settings'):
3749 url = os.path.join(url, 'codereview.settings')
3750
3751 # Load code review settings and download hooks (if available).
3752 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3753 return 0
3754
3755
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003756def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003757 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003758 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3759 branch = ShortBranchName(branchref)
3760 _, args = parser.parse_args(args)
3761 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003762 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003763 return RunGit(['config', 'branch.%s.base-url' % branch],
3764 error_ok=False).strip()
3765 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003766 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003767 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3768 error_ok=False).strip()
3769
3770
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003771def color_for_status(status):
3772 """Maps a Changelist status to color, for CMDstatus and other tools."""
3773 return {
3774 'unsent': Fore.RED,
3775 'waiting': Fore.BLUE,
3776 'reply': Fore.YELLOW,
3777 'lgtm': Fore.GREEN,
3778 'commit': Fore.MAGENTA,
3779 'closed': Fore.CYAN,
3780 'error': Fore.WHITE,
3781 }.get(status, Fore.WHITE)
3782
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003783
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003784def get_cl_statuses(changes, fine_grained, max_processes=None):
3785 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003786
3787 If fine_grained is true, this will fetch CL statuses from the server.
3788 Otherwise, simply indicate if there's a matching url for the given branches.
3789
3790 If max_processes is specified, it is used as the maximum number of processes
3791 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3792 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003793
3794 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003795 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003796 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003797 upload.verbosity = 0
3798
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003799 if not changes:
3800 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003801
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003802 if not fine_grained:
3803 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003804 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003805 for cl in changes:
3806 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003807 return
3808
3809 # First, sort out authentication issues.
3810 logging.debug('ensuring credentials exist')
3811 for cl in changes:
3812 cl.EnsureAuthenticated(force=False, refresh=True)
3813
3814 def fetch(cl):
3815 try:
3816 return (cl, cl.GetStatus())
3817 except:
3818 # See http://crbug.com/629863.
3819 logging.exception('failed to fetch status for %s:', cl)
3820 raise
3821
3822 threads_count = len(changes)
3823 if max_processes:
3824 threads_count = max(1, min(threads_count, max_processes))
3825 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3826
3827 pool = ThreadPool(threads_count)
3828 fetched_cls = set()
3829 try:
3830 it = pool.imap_unordered(fetch, changes).__iter__()
3831 while True:
3832 try:
3833 cl, status = it.next(timeout=5)
3834 except multiprocessing.TimeoutError:
3835 break
3836 fetched_cls.add(cl)
3837 yield cl, status
3838 finally:
3839 pool.close()
3840
3841 # Add any branches that failed to fetch.
3842 for cl in set(changes) - fetched_cls:
3843 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003844
rmistry@google.com2dd99862015-06-22 12:22:18 +00003845
3846def upload_branch_deps(cl, args):
3847 """Uploads CLs of local branches that are dependents of the current branch.
3848
3849 If the local branch dependency tree looks like:
3850 test1 -> test2.1 -> test3.1
3851 -> test3.2
3852 -> test2.2 -> test3.3
3853
3854 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3855 run on the dependent branches in this order:
3856 test2.1, test3.1, test3.2, test2.2, test3.3
3857
3858 Note: This function does not rebase your local dependent branches. Use it when
3859 you make a change to the parent branch that will not conflict with its
3860 dependent branches, and you would like their dependencies updated in
3861 Rietveld.
3862 """
3863 if git_common.is_dirty_git_tree('upload-branch-deps'):
3864 return 1
3865
3866 root_branch = cl.GetBranch()
3867 if root_branch is None:
3868 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3869 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003870 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003871 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3872 'patchset dependencies without an uploaded CL.')
3873
3874 branches = RunGit(['for-each-ref',
3875 '--format=%(refname:short) %(upstream:short)',
3876 'refs/heads'])
3877 if not branches:
3878 print('No local branches found.')
3879 return 0
3880
3881 # Create a dictionary of all local branches to the branches that are dependent
3882 # on it.
3883 tracked_to_dependents = collections.defaultdict(list)
3884 for b in branches.splitlines():
3885 tokens = b.split()
3886 if len(tokens) == 2:
3887 branch_name, tracked = tokens
3888 tracked_to_dependents[tracked].append(branch_name)
3889
vapiera7fbd5a2016-06-16 09:17:49 -07003890 print()
3891 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003892 dependents = []
3893 def traverse_dependents_preorder(branch, padding=''):
3894 dependents_to_process = tracked_to_dependents.get(branch, [])
3895 padding += ' '
3896 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003897 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003898 dependents.append(dependent)
3899 traverse_dependents_preorder(dependent, padding)
3900 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003901 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003902
3903 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003904 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003905 return 0
3906
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003907 confirm_or_exit('This command will checkout all dependent branches and run '
3908 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003909
andybons@chromium.org962f9462016-02-03 20:00:42 +00003910 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003911 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003912 args.extend(['-t', 'Updated patchset dependency'])
3913
rmistry@google.com2dd99862015-06-22 12:22:18 +00003914 # Record all dependents that failed to upload.
3915 failures = {}
3916 # Go through all dependents, checkout the branch and upload.
3917 try:
3918 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003919 print()
3920 print('--------------------------------------')
3921 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003922 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003923 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003924 try:
3925 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003926 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003927 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003928 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003929 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003931 finally:
3932 # Swap back to the original root branch.
3933 RunGit(['checkout', '-q', root_branch])
3934
vapiera7fbd5a2016-06-16 09:17:49 -07003935 print()
3936 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003937 for dependent_branch in dependents:
3938 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003939 print(' %s : %s' % (dependent_branch, upload_status))
3940 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003941
3942 return 0
3943
3944
kmarshall3bff56b2016-06-06 18:31:47 -07003945def CMDarchive(parser, args):
3946 """Archives and deletes branches associated with closed changelists."""
3947 parser.add_option(
3948 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003949 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003950 parser.add_option(
3951 '-f', '--force', action='store_true',
3952 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003953 parser.add_option(
3954 '-d', '--dry-run', action='store_true',
3955 help='Skip the branch tagging and removal steps.')
3956 parser.add_option(
3957 '-t', '--notags', action='store_true',
3958 help='Do not tag archived branches. '
3959 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003960
3961 auth.add_auth_options(parser)
3962 options, args = parser.parse_args(args)
3963 if args:
3964 parser.error('Unsupported args: %s' % ' '.join(args))
3965 auth_config = auth.extract_auth_config_from_options(options)
3966
3967 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3968 if not branches:
3969 return 0
3970
vapiera7fbd5a2016-06-16 09:17:49 -07003971 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003972 changes = [Changelist(branchref=b, auth_config=auth_config)
3973 for b in branches.splitlines()]
3974 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3975 statuses = get_cl_statuses(changes,
3976 fine_grained=True,
3977 max_processes=options.maxjobs)
3978 proposal = [(cl.GetBranch(),
3979 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3980 for cl, status in statuses
3981 if status == 'closed']
3982 proposal.sort()
3983
3984 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003985 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003986 return 0
3987
3988 current_branch = GetCurrentBranch()
3989
vapiera7fbd5a2016-06-16 09:17:49 -07003990 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003991 if options.notags:
3992 for next_item in proposal:
3993 print(' ' + next_item[0])
3994 else:
3995 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3996 for next_item in proposal:
3997 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003998
kmarshall9249e012016-08-23 12:02:16 -07003999 # Quit now on precondition failure or if instructed by the user, either
4000 # via an interactive prompt or by command line flags.
4001 if options.dry_run:
4002 print('\nNo changes were made (dry run).\n')
4003 return 0
4004 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004005 print('You are currently on a branch \'%s\' which is associated with a '
4006 'closed codereview issue, so archive cannot proceed. Please '
4007 'checkout another branch and run this command again.' %
4008 current_branch)
4009 return 1
kmarshall9249e012016-08-23 12:02:16 -07004010 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004011 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4012 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004013 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004014 return 1
4015
4016 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004017 if not options.notags:
4018 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004019 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004020
vapiera7fbd5a2016-06-16 09:17:49 -07004021 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004022
4023 return 0
4024
4025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004027 """Show status of changelists.
4028
4029 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004030 - Red not sent for review or broken
4031 - Blue waiting for review
4032 - Yellow waiting for you to reply to review
4033 - Green LGTM'ed
4034 - Magenta in the commit queue
4035 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004036
4037 Also see 'git cl comments'.
4038 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004039 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004040 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004041 parser.add_option('-f', '--fast', action='store_true',
4042 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004043 parser.add_option(
4044 '-j', '--maxjobs', action='store', type=int,
4045 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004046
4047 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004048 _add_codereview_issue_select_options(
4049 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004050 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004051 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004052 if args:
4053 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004054 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055
iannuccie53c9352016-08-17 14:40:40 -07004056 if options.issue is not None and not options.field:
4057 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004060 cl = Changelist(auth_config=auth_config, issue=options.issue,
4061 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004063 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064 elif options.field == 'id':
4065 issueid = cl.GetIssue()
4066 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068 elif options.field == 'patch':
4069 patchset = cl.GetPatchset()
4070 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004072 elif options.field == 'status':
4073 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074 elif options.field == 'url':
4075 url = cl.GetIssueURL()
4076 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004078 return 0
4079
4080 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4081 if not branches:
4082 print('No local branch found.')
4083 return 0
4084
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004085 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004086 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004087 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004088 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004089 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004090 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004091 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004092
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004093 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004094 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4095 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4096 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004097 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004098 c, status = output.next()
4099 branch_statuses[c.GetBranch()] = status
4100 status = branch_statuses.pop(branch)
4101 url = cl.GetIssueURL()
4102 if url and (not status or status == 'error'):
4103 # The issue probably doesn't exist anymore.
4104 url += ' (broken)'
4105
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004106 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004107 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004108 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004109 color = ''
4110 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004111 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004112 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004113 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004114 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004115
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004116
4117 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004118 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004119 print('Current branch: %s' % branch)
4120 for cl in changes:
4121 if cl.GetBranch() == branch:
4122 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004123 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004124 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004125 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004127 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004128 print('Issue description:')
4129 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130 return 0
4131
4132
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004133def colorize_CMDstatus_doc():
4134 """To be called once in main() to add colors to git cl status help."""
4135 colors = [i for i in dir(Fore) if i[0].isupper()]
4136
4137 def colorize_line(line):
4138 for color in colors:
4139 if color in line.upper():
4140 # Extract whitespaces first and the leading '-'.
4141 indent = len(line) - len(line.lstrip(' ')) + 1
4142 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4143 return line
4144
4145 lines = CMDstatus.__doc__.splitlines()
4146 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4147
4148
phajdan.jre328cf92016-08-22 04:12:17 -07004149def write_json(path, contents):
4150 with open(path, 'w') as f:
4151 json.dump(contents, f)
4152
4153
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004154@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004155def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004156 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004157
4158 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004159 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004160 parser.add_option('-r', '--reverse', action='store_true',
4161 help='Lookup the branch(es) for the specified issues. If '
4162 'no issues are specified, all branches with mapped '
4163 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004164 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004165 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004166 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004167 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168
dnj@chromium.org406c4402015-03-03 17:22:28 +00004169 if options.reverse:
4170 branches = RunGit(['for-each-ref', 'refs/heads',
4171 '--format=%(refname:short)']).splitlines()
4172
4173 # Reverse issue lookup.
4174 issue_branch_map = {}
4175 for branch in branches:
4176 cl = Changelist(branchref=branch)
4177 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4178 if not args:
4179 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004180 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004181 for issue in args:
4182 if not issue:
4183 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004184 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004185 print('Branch for issue number %s: %s' % (
4186 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004187 if options.json:
4188 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004189 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004190 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004191 if len(args) > 0:
4192 try:
4193 issue = int(args[0])
4194 except ValueError:
4195 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004196 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004197 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004198 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004199 if options.json:
4200 write_json(options.json, {
4201 'issue': cl.GetIssue(),
4202 'issue_url': cl.GetIssueURL(),
4203 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 return 0
4205
4206
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004207def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004208 """Shows or posts review comments for any changelist."""
4209 parser.add_option('-a', '--add-comment', dest='comment',
4210 help='comment to add to an issue')
4211 parser.add_option('-i', dest='issue',
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004212 help='review issue id (defaults to current issue)')
smut@google.comc85ac942015-09-15 16:34:43 +00004213 parser.add_option('-j', '--json-file',
4214 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004215 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004216 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004217 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004218 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004219 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004220
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004221 issue = None
4222 if options.issue:
4223 try:
4224 issue = int(options.issue)
4225 except ValueError:
4226 DieWithError('A review issue id is expected to be a number')
4227
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004228 cl = Changelist(issue=issue,
4229 # TODO(tandrii): remove 'rietveld' default.
4230 codereview=options.forced_codereview or 'rietveld',
4231 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004232
4233 if options.comment:
4234 cl.AddComment(options.comment)
4235 return 0
4236
4237 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00004238 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00004239 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00004240 summary.append({
4241 'date': message['date'],
4242 'lgtm': False,
4243 'message': message['text'],
4244 'not_lgtm': False,
4245 'sender': message['sender'],
4246 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004247 if message['disapproval']:
4248 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00004249 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004250 elif message['approval']:
4251 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00004252 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004253 elif message['sender'] == data['owner_email']:
4254 color = Fore.MAGENTA
4255 else:
4256 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07004257 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004258 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07004259 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004260 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004261 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00004262 if options.json_file:
4263 with open(options.json_file, 'wb') as f:
4264 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004265 return 0
4266
4267
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004268@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004269def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004270 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004271 parser.add_option('-d', '--display', action='store_true',
4272 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004273 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004274 help='New description to set for this issue (- for stdin, '
4275 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004276 parser.add_option('-f', '--force', action='store_true',
4277 help='Delete any unpublished Gerrit edits for this issue '
4278 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004279
4280 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004281 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004282 options, args = parser.parse_args(args)
4283 _process_codereview_select_options(parser, options)
4284
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004285 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004286 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004287 target_issue_arg = ParseIssueNumberArgument(args[0])
4288 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004289 parser.print_help()
4290 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004291
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004292 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004293
martiniss6eda05f2016-06-30 10:18:35 -07004294 kwargs = {
4295 'auth_config': auth_config,
4296 'codereview': options.forced_codereview,
4297 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004298 if target_issue_arg:
4299 kwargs['issue'] = target_issue_arg.issue
4300 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004301
4302 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004303
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004304 if not cl.GetIssue():
4305 DieWithError('This branch has no associated changelist.')
4306 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004307
smut@google.com34fb6b12015-07-13 20:03:26 +00004308 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004310 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004311
4312 if options.new_description:
4313 text = options.new_description
4314 if text == '-':
4315 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004316 elif text == '+':
4317 base_branch = cl.GetCommonAncestorWithUpstream()
4318 change = cl.GetChange(base_branch, None, local_description=True)
4319 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004320
4321 description.set_description(text)
4322 else:
4323 description.prompt()
4324
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004325 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004326 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004327 return 0
4328
4329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330def CreateDescriptionFromLog(args):
4331 """Pulls out the commit log to use as a base for the CL description."""
4332 log_args = []
4333 if len(args) == 1 and not args[0].endswith('.'):
4334 log_args = [args[0] + '..']
4335 elif len(args) == 1 and args[0].endswith('...'):
4336 log_args = [args[0][:-1]]
4337 elif len(args) == 2:
4338 log_args = [args[0] + '..' + args[1]]
4339 else:
4340 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004341 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004342
4343
thestig@chromium.org44202a22014-03-11 19:22:18 +00004344def CMDlint(parser, args):
4345 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004346 parser.add_option('--filter', action='append', metavar='-x,+y',
4347 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004348 auth.add_auth_options(parser)
4349 options, args = parser.parse_args(args)
4350 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004351
4352 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004353 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004354 try:
4355 import cpplint
4356 import cpplint_chromium
4357 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004358 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004359 return 1
4360
4361 # Change the current working directory before calling lint so that it
4362 # shows the correct base.
4363 previous_cwd = os.getcwd()
4364 os.chdir(settings.GetRoot())
4365 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004366 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004367 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4368 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004369 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004370 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004371 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004372
4373 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004374 command = args + files
4375 if options.filter:
4376 command = ['--filter=' + ','.join(options.filter)] + command
4377 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004378
4379 white_regex = re.compile(settings.GetLintRegex())
4380 black_regex = re.compile(settings.GetLintIgnoreRegex())
4381 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4382 for filename in filenames:
4383 if white_regex.match(filename):
4384 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004386 else:
4387 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4388 extra_check_functions)
4389 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004390 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004391 finally:
4392 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004394 if cpplint._cpplint_state.error_count != 0:
4395 return 1
4396 return 0
4397
4398
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004400 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004401 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004402 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004403 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004404 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004405 auth.add_auth_options(parser)
4406 options, args = parser.parse_args(args)
4407 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004408
sbc@chromium.org71437c02015-04-09 19:29:40 +00004409 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411 return 1
4412
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004413 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004414 if args:
4415 base_branch = args[0]
4416 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004417 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004418 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004419
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004420 cl.RunHook(
4421 committing=not options.upload,
4422 may_prompt=False,
4423 verbose=options.verbose,
4424 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004425 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426
4427
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004428def GenerateGerritChangeId(message):
4429 """Returns Ixxxxxx...xxx change id.
4430
4431 Works the same way as
4432 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4433 but can be called on demand on all platforms.
4434
4435 The basic idea is to generate git hash of a state of the tree, original commit
4436 message, author/committer info and timestamps.
4437 """
4438 lines = []
4439 tree_hash = RunGitSilent(['write-tree'])
4440 lines.append('tree %s' % tree_hash.strip())
4441 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4442 if code == 0:
4443 lines.append('parent %s' % parent.strip())
4444 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4445 lines.append('author %s' % author.strip())
4446 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4447 lines.append('committer %s' % committer.strip())
4448 lines.append('')
4449 # Note: Gerrit's commit-hook actually cleans message of some lines and
4450 # whitespace. This code is not doing this, but it clearly won't decrease
4451 # entropy.
4452 lines.append(message)
4453 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4454 stdin='\n'.join(lines))
4455 return 'I%s' % change_hash.strip()
4456
4457
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004458def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004459 """Computes the remote branch ref to use for the CL.
4460
4461 Args:
4462 remote (str): The git remote for the CL.
4463 remote_branch (str): The git remote branch for the CL.
4464 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004465 """
4466 if not (remote and remote_branch):
4467 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004468
wittman@chromium.org455dc922015-01-26 20:15:50 +00004469 if target_branch:
4470 # Cannonicalize branch references to the equivalent local full symbolic
4471 # refs, which are then translated into the remote full symbolic refs
4472 # below.
4473 if '/' not in target_branch:
4474 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4475 else:
4476 prefix_replacements = (
4477 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4478 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4479 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4480 )
4481 match = None
4482 for regex, replacement in prefix_replacements:
4483 match = re.search(regex, target_branch)
4484 if match:
4485 remote_branch = target_branch.replace(match.group(0), replacement)
4486 break
4487 if not match:
4488 # This is a branch path but not one we recognize; use as-is.
4489 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004490 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4491 # Handle the refs that need to land in different refs.
4492 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004493
wittman@chromium.org455dc922015-01-26 20:15:50 +00004494 # Create the true path to the remote branch.
4495 # Does the following translation:
4496 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4497 # * refs/remotes/origin/master -> refs/heads/master
4498 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4499 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4500 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4501 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4502 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4503 'refs/heads/')
4504 elif remote_branch.startswith('refs/remotes/branch-heads'):
4505 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004506
wittman@chromium.org455dc922015-01-26 20:15:50 +00004507 return remote_branch
4508
4509
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004510def cleanup_list(l):
4511 """Fixes a list so that comma separated items are put as individual items.
4512
4513 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4514 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4515 """
4516 items = sum((i.split(',') for i in l), [])
4517 stripped_items = (i.strip() for i in items)
4518 return sorted(filter(None, stripped_items))
4519
4520
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004521@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004522def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004523 """Uploads the current changelist to codereview.
4524
4525 Can skip dependency patchset uploads for a branch by running:
4526 git config branch.branch_name.skip-deps-uploads True
4527 To unset run:
4528 git config --unset branch.branch_name.skip-deps-uploads
4529 Can also set the above globally by using the --global flag.
4530 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004531 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4532 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004533 parser.add_option('--bypass-watchlists', action='store_true',
4534 dest='bypass_watchlists',
4535 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004536 parser.add_option('-f', action='store_true', dest='force',
4537 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004538 parser.add_option('--message', '-m', dest='message',
4539 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004540 parser.add_option('-b', '--bug',
4541 help='pre-populate the bug number(s) for this issue. '
4542 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004543 parser.add_option('--message-file', dest='message_file',
4544 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004545 parser.add_option('--title', '-t', dest='title',
4546 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004547 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004548 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004549 help='reviewer email addresses')
4550 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004551 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004552 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004553 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004554 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004555 parser.add_option('--emulate_svn_auto_props',
4556 '--emulate-svn-auto-props',
4557 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004558 dest="emulate_svn_auto_props",
4559 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004560 parser.add_option('-c', '--use-commit-queue', action='store_true',
4561 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004562 parser.add_option('--private', action='store_true',
4563 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004564 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004565 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004566 metavar='TARGET',
4567 help='Apply CL to remote ref TARGET. ' +
4568 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004569 parser.add_option('--squash', action='store_true',
4570 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004571 parser.add_option('--no-squash', action='store_true',
4572 help='Don\'t squash multiple commits into one ' +
4573 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004574 parser.add_option('--topic', default=None,
4575 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004576 parser.add_option('--email', default=None,
4577 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004578 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4579 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004580 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4581 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004582 help='Send the patchset to do a CQ dry run right after '
4583 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004584 parser.add_option('--dependencies', action='store_true',
4585 help='Uploads CLs of all the local branches that depend on '
4586 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004587
rmistry@google.com2dd99862015-06-22 12:22:18 +00004588 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004589 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004590 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004591 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004592 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004593 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004594 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004595
sbc@chromium.org71437c02015-04-09 19:29:40 +00004596 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004597 return 1
4598
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004599 options.reviewers = cleanup_list(options.reviewers)
4600 options.cc = cleanup_list(options.cc)
4601
tandriib80458a2016-06-23 12:20:07 -07004602 if options.message_file:
4603 if options.message:
4604 parser.error('only one of --message and --message-file allowed.')
4605 options.message = gclient_utils.FileRead(options.message_file)
4606 options.message_file = None
4607
tandrii4d0545a2016-07-06 03:56:49 -07004608 if options.cq_dry_run and options.use_commit_queue:
4609 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4610
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004611 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4612 settings.GetIsGerrit()
4613
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004614 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004615 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004616
4617
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004618@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004619def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004620 """DEPRECATED: Used to commit the current changelist via git-svn."""
4621 message = ('git-cl no longer supports committing to SVN repositories via '
4622 'git-svn. You probably want to use `git cl land` instead.')
4623 print(message)
4624 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004625
4626
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004627@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004628def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004629 """Commits the current changelist via git.
4630
4631 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4632 upstream and closes the issue automatically and atomically.
4633
4634 Otherwise (in case of Rietveld):
4635 Squashes branch into a single commit.
4636 Updates commit message with metadata (e.g. pointer to review).
4637 Pushes the code upstream.
4638 Updates review and closes.
4639 """
4640 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4641 help='bypass upload presubmit hook')
4642 parser.add_option('-m', dest='message',
4643 help="override review description")
4644 parser.add_option('-f', action='store_true', dest='force',
4645 help="force yes to questions (don't prompt)")
4646 parser.add_option('-c', dest='contributor',
4647 help="external contributor for patch (appended to " +
4648 "description and used as author for git). Should be " +
4649 "formatted as 'First Last <email@example.com>'")
4650 add_git_similarity(parser)
4651 auth.add_auth_options(parser)
4652 (options, args) = parser.parse_args(args)
4653 auth_config = auth.extract_auth_config_from_options(options)
4654
4655 cl = Changelist(auth_config=auth_config)
4656
4657 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4658 if cl.IsGerrit():
4659 if options.message:
4660 # This could be implemented, but it requires sending a new patch to
4661 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4662 # Besides, Gerrit has the ability to change the commit message on submit
4663 # automatically, thus there is no need to support this option (so far?).
4664 parser.error('-m MESSAGE option is not supported for Gerrit.')
4665 if options.contributor:
4666 parser.error(
4667 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4668 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4669 'the contributor\'s "name <email>". If you can\'t upload such a '
4670 'commit for review, contact your repository admin and request'
4671 '"Forge-Author" permission.')
4672 if not cl.GetIssue():
4673 DieWithError('You must upload the change first to Gerrit.\n'
4674 ' If you would rather have `git cl land` upload '
4675 'automatically for you, see http://crbug.com/642759')
4676 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4677 options.verbose)
4678
4679 current = cl.GetBranch()
4680 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4681 if remote == '.':
4682 print()
4683 print('Attempting to push branch %r into another local branch!' % current)
4684 print()
4685 print('Either reparent this branch on top of origin/master:')
4686 print(' git reparent-branch --root')
4687 print()
4688 print('OR run `git rebase-update` if you think the parent branch is ')
4689 print('already committed.')
4690 print()
4691 print(' Current parent: %r' % upstream_branch)
4692 return 1
4693
4694 if not args:
4695 # Default to merging against our best guess of the upstream branch.
4696 args = [cl.GetUpstreamBranch()]
4697
4698 if options.contributor:
4699 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4700 print("Please provide contibutor as 'First Last <email@example.com>'")
4701 return 1
4702
4703 base_branch = args[0]
4704
4705 if git_common.is_dirty_git_tree('land'):
4706 return 1
4707
4708 # This rev-list syntax means "show all commits not in my branch that
4709 # are in base_branch".
4710 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4711 base_branch]).splitlines()
4712 if upstream_commits:
4713 print('Base branch "%s" has %d commits '
4714 'not in this branch.' % (base_branch, len(upstream_commits)))
4715 print('Run "git merge %s" before attempting to land.' % base_branch)
4716 return 1
4717
4718 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4719 if not options.bypass_hooks:
4720 author = None
4721 if options.contributor:
4722 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4723 hook_results = cl.RunHook(
4724 committing=True,
4725 may_prompt=not options.force,
4726 verbose=options.verbose,
4727 change=cl.GetChange(merge_base, author))
4728 if not hook_results.should_continue():
4729 return 1
4730
4731 # Check the tree status if the tree status URL is set.
4732 status = GetTreeStatus()
4733 if 'closed' == status:
4734 print('The tree is closed. Please wait for it to reopen. Use '
4735 '"git cl land --bypass-hooks" to commit on a closed tree.')
4736 return 1
4737 elif 'unknown' == status:
4738 print('Unable to determine tree status. Please verify manually and '
4739 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4740 return 1
4741
4742 change_desc = ChangeDescription(options.message)
4743 if not change_desc.description and cl.GetIssue():
4744 change_desc = ChangeDescription(cl.GetDescription())
4745
4746 if not change_desc.description:
4747 if not cl.GetIssue() and options.bypass_hooks:
4748 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4749 else:
4750 print('No description set.')
4751 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4752 return 1
4753
4754 # Keep a separate copy for the commit message, because the commit message
4755 # contains the link to the Rietveld issue, while the Rietveld message contains
4756 # the commit viewvc url.
4757 if cl.GetIssue():
4758 change_desc.update_reviewers(cl.GetApprovingReviewers())
4759
4760 commit_desc = ChangeDescription(change_desc.description)
4761 if cl.GetIssue():
4762 # Xcode won't linkify this URL unless there is a non-whitespace character
4763 # after it. Add a period on a new line to circumvent this. Also add a space
4764 # before the period to make sure that Gitiles continues to correctly resolve
4765 # the URL.
4766 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4767 if options.contributor:
4768 commit_desc.append_footer('Patch from %s.' % options.contributor)
4769
4770 print('Description:')
4771 print(commit_desc.description)
4772
4773 branches = [merge_base, cl.GetBranchRef()]
4774 if not options.force:
4775 print_stats(options.similarity, options.find_copies, branches)
4776
4777 # We want to squash all this branch's commits into one commit with the proper
4778 # description. We do this by doing a "reset --soft" to the base branch (which
4779 # keeps the working copy the same), then landing that.
4780 MERGE_BRANCH = 'git-cl-commit'
4781 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4782 # Delete the branches if they exist.
4783 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4784 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4785 result = RunGitWithCode(showref_cmd)
4786 if result[0] == 0:
4787 RunGit(['branch', '-D', branch])
4788
4789 # We might be in a directory that's present in this branch but not in the
4790 # trunk. Move up to the top of the tree so that git commands that expect a
4791 # valid CWD won't fail after we check out the merge branch.
4792 rel_base_path = settings.GetRelativeRoot()
4793 if rel_base_path:
4794 os.chdir(rel_base_path)
4795
4796 # Stuff our change into the merge branch.
4797 # We wrap in a try...finally block so if anything goes wrong,
4798 # we clean up the branches.
4799 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004800 revision = None
4801 try:
4802 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4803 RunGit(['reset', '--soft', merge_base])
4804 if options.contributor:
4805 RunGit(
4806 [
4807 'commit', '--author', options.contributor,
4808 '-m', commit_desc.description,
4809 ])
4810 else:
4811 RunGit(['commit', '-m', commit_desc.description])
4812
4813 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4814 mirror = settings.GetGitMirror(remote)
4815 if mirror:
4816 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004817 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004818 else:
4819 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004820 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004821 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4822
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004823 if git_numberer_enabled:
4824 # TODO(tandrii): maybe do autorebase + retry on failure
4825 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004826 logging.debug('Adding git number footers')
4827 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4828 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4829 branch)
4830 # Ensure timestamps are monotonically increasing.
4831 timestamp = max(1 + _get_committer_timestamp(merge_base),
4832 _get_committer_timestamp('HEAD'))
4833 _git_amend_head(commit_desc.description, timestamp)
4834 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004835
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004836 retcode, output = RunGitWithCode(
4837 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004838 if retcode == 0:
4839 revision = RunGit(['rev-parse', 'HEAD']).strip()
4840 logging.debug(output)
4841 except: # pylint: disable=bare-except
4842 if _IS_BEING_TESTED:
4843 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4844 + '-' * 30 + '8<' + '-' * 30)
4845 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4846 raise
4847 finally:
4848 # And then swap back to the original branch and clean up.
4849 RunGit(['checkout', '-q', cl.GetBranch()])
4850 RunGit(['branch', '-D', MERGE_BRANCH])
4851
4852 if not revision:
4853 print('Failed to push. If this persists, please file a bug.')
4854 return 1
4855
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004856 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004857 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004858 if viewvc_url and revision:
4859 change_desc.append_footer(
4860 'Committed: %s%s' % (viewvc_url, revision))
4861 elif revision:
4862 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004863 print('Closing issue '
4864 '(you may be prompted for your codereview password)...')
4865 cl.UpdateDescription(change_desc.description)
4866 cl.CloseIssue()
4867 props = cl.GetIssueProperties()
4868 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004869 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4870 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004871 if options.bypass_hooks:
4872 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4873 else:
4874 comment += ' (presubmit successful).'
4875 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4876
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004877 if os.path.isfile(POSTUPSTREAM_HOOK):
4878 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4879
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004880 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004881
4882
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004883@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004884def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004885 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004886 parser.add_option('-b', dest='newbranch',
4887 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004888 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004889 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004890 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4891 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004892 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004893 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004894 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004895 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004896 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004897 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004898
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004899
4900 group = optparse.OptionGroup(
4901 parser,
4902 'Options for continuing work on the current issue uploaded from a '
4903 'different clone (e.g. different machine). Must be used independently '
4904 'from the other options. No issue number should be specified, and the '
4905 'branch must have an issue number associated with it')
4906 group.add_option('--reapply', action='store_true', dest='reapply',
4907 help='Reset the branch and reapply the issue.\n'
4908 'CAUTION: This will undo any local changes in this '
4909 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004910
4911 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004912 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004913 parser.add_option_group(group)
4914
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004915 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004916 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004917 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004918 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004919 auth_config = auth.extract_auth_config_from_options(options)
4920
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004921
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004922 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004923 if options.newbranch:
4924 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004925 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004926 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004927
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004928 cl = Changelist(auth_config=auth_config,
4929 codereview=options.forced_codereview)
4930 if not cl.GetIssue():
4931 parser.error('current branch must have an associated issue')
4932
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004933 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004934 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004935 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004936
4937 RunGit(['reset', '--hard', upstream])
4938 if options.pull:
4939 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004940
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004941 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4942 options.directory)
4943
4944 if len(args) != 1 or not args[0]:
4945 parser.error('Must specify issue number or url')
4946
4947 # We don't want uncommitted changes mixed up with the patch.
4948 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004949 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004950
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004951 if options.newbranch:
4952 if options.force:
4953 RunGit(['branch', '-D', options.newbranch],
4954 stderr=subprocess2.PIPE, error_ok=True)
4955 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004956 elif not GetCurrentBranch():
4957 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004958
4959 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4960
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004961 if cl.IsGerrit():
4962 if options.reject:
4963 parser.error('--reject is not supported with Gerrit codereview.')
4964 if options.nocommit:
4965 parser.error('--nocommit is not supported with Gerrit codereview.')
4966 if options.directory:
4967 parser.error('--directory is not supported with Gerrit codereview.')
4968
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004969 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004970 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004971
4972
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004973def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004974 """Fetches the tree status and returns either 'open', 'closed',
4975 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004976 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004977 if url:
4978 status = urllib2.urlopen(url).read().lower()
4979 if status.find('closed') != -1 or status == '0':
4980 return 'closed'
4981 elif status.find('open') != -1 or status == '1':
4982 return 'open'
4983 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004984 return 'unset'
4985
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004986
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004987def GetTreeStatusReason():
4988 """Fetches the tree status from a json url and returns the message
4989 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004990 url = settings.GetTreeStatusUrl()
4991 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004992 connection = urllib2.urlopen(json_url)
4993 status = json.loads(connection.read())
4994 connection.close()
4995 return status['message']
4996
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004997
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004998def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004999 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005000 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005001 status = GetTreeStatus()
5002 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005003 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005004 return 2
5005
vapiera7fbd5a2016-06-16 09:17:49 -07005006 print('The tree is %s' % status)
5007 print()
5008 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005009 if status != 'open':
5010 return 1
5011 return 0
5012
5013
maruel@chromium.org15192402012-09-06 12:38:29 +00005014def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005015 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005016 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005017 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005018 '-b', '--bot', action='append',
5019 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5020 'times to specify multiple builders. ex: '
5021 '"-b win_rel -b win_layout". See '
5022 'the try server waterfall for the builders name and the tests '
5023 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005024 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005025 '-B', '--bucket', default='',
5026 help=('Buildbucket bucket to send the try requests.'))
5027 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005028 '-m', '--master', default='',
5029 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005030 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005031 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005032 help='Revision to use for the try job; default: the revision will '
5033 'be determined by the try recipe that builder runs, which usually '
5034 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005035 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005036 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005037 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005038 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005039 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005040 '--project',
5041 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005042 'in recipe to determine to which repository or directory to '
5043 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005044 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005045 '-p', '--property', dest='properties', action='append', default=[],
5046 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005047 'key2=value2 etc. The value will be treated as '
5048 'json if decodable, or as string otherwise. '
5049 'NOTE: using this may make your try job not usable for CQ, '
5050 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005051 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005052 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5053 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005054 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005055 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005056 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005057 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005058
machenbach@chromium.org45453142015-09-15 08:45:22 +00005059 # Make sure that all properties are prop=value pairs.
5060 bad_params = [x for x in options.properties if '=' not in x]
5061 if bad_params:
5062 parser.error('Got properties with missing "=": %s' % bad_params)
5063
maruel@chromium.org15192402012-09-06 12:38:29 +00005064 if args:
5065 parser.error('Unknown arguments: %s' % args)
5066
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005067 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005068 if not cl.GetIssue():
5069 parser.error('Need to upload first')
5070
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005071 if cl.IsGerrit():
5072 # HACK: warm up Gerrit change detail cache to save on RPCs.
5073 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5074
tandriie113dfd2016-10-11 10:20:12 -07005075 error_message = cl.CannotTriggerTryJobReason()
5076 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005077 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005078
borenet6c0efe62016-10-19 08:13:29 -07005079 if options.bucket and options.master:
5080 parser.error('Only one of --bucket and --master may be used.')
5081
qyearsley1fdfcb62016-10-24 13:22:03 -07005082 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005083
qyearsleydd49f942016-10-28 11:57:22 -07005084 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5085 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005086 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005087 if options.verbose:
5088 print('git cl try with no bots now defaults to CQ Dry Run.')
5089 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005090
borenet6c0efe62016-10-19 08:13:29 -07005091 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005092 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005093 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005094 'of bot requires an initial job from a parent (usually a builder). '
5095 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005096 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005097 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005098
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005099 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005100 # TODO(tandrii): Checking local patchset against remote patchset is only
5101 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5102 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005103 print('Warning: Codereview server has newer patchsets (%s) than most '
5104 'recent upload from local checkout (%s). Did a previous upload '
5105 'fail?\n'
5106 'By default, git cl try uses the latest patchset from '
5107 'codereview, continuing to use patchset %s.\n' %
5108 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005109
tandrii568043b2016-10-11 07:49:18 -07005110 try:
borenet6c0efe62016-10-19 08:13:29 -07005111 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5112 patchset)
tandrii568043b2016-10-11 07:49:18 -07005113 except BuildbucketResponseException as ex:
5114 print('ERROR: %s' % ex)
5115 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005116 return 0
5117
5118
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005119def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005120 """Prints info about try jobs associated with current CL."""
5121 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005122 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005123 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005124 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005125 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005126 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005127 '--color', action='store_true', default=setup_color.IS_TTY,
5128 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005129 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005130 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5131 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005132 group.add_option(
5133 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005134 parser.add_option_group(group)
5135 auth.add_auth_options(parser)
5136 options, args = parser.parse_args(args)
5137 if args:
5138 parser.error('Unrecognized args: %s' % ' '.join(args))
5139
5140 auth_config = auth.extract_auth_config_from_options(options)
5141 cl = Changelist(auth_config=auth_config)
5142 if not cl.GetIssue():
5143 parser.error('Need to upload first')
5144
tandrii221ab252016-10-06 08:12:04 -07005145 patchset = options.patchset
5146 if not patchset:
5147 patchset = cl.GetMostRecentPatchset()
5148 if not patchset:
5149 parser.error('Codereview doesn\'t know about issue %s. '
5150 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005151 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005152 cl.GetIssue())
5153
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005154 # TODO(tandrii): Checking local patchset against remote patchset is only
5155 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5156 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005157 print('Warning: Codereview server has newer patchsets (%s) than most '
5158 'recent upload from local checkout (%s). Did a previous upload '
5159 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005160 'By default, git cl try-results uses the latest patchset from '
5161 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005162 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005163 try:
tandrii221ab252016-10-06 08:12:04 -07005164 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005165 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005166 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005167 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005168 if options.json:
5169 write_try_results_json(options.json, jobs)
5170 else:
5171 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005172 return 0
5173
5174
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005175@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005176def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005177 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005178 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005179 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005180 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005182 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005183 if args:
5184 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005185 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005186 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005187 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005188 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005189
5190 # Clear configured merge-base, if there is one.
5191 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005192 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005193 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005194 return 0
5195
5196
thestig@chromium.org00858c82013-12-02 23:08:03 +00005197def CMDweb(parser, args):
5198 """Opens the current CL in the web browser."""
5199 _, args = parser.parse_args(args)
5200 if args:
5201 parser.error('Unrecognized args: %s' % ' '.join(args))
5202
5203 issue_url = Changelist().GetIssueURL()
5204 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005205 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005206 return 1
5207
5208 webbrowser.open(issue_url)
5209 return 0
5210
5211
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005212def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005213 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005214 parser.add_option('-d', '--dry-run', action='store_true',
5215 help='trigger in dry run mode')
5216 parser.add_option('-c', '--clear', action='store_true',
5217 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005218 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005219 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005220 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005221 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005222 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005223 if args:
5224 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005225 if options.dry_run and options.clear:
5226 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5227
iannuccie53c9352016-08-17 14:40:40 -07005228 cl = Changelist(auth_config=auth_config, issue=options.issue,
5229 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005230 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005231 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005232 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005233 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005234 state = _CQState.DRY_RUN
5235 else:
5236 state = _CQState.COMMIT
5237 if not cl.GetIssue():
5238 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005239 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005240 return 0
5241
5242
groby@chromium.org411034a2013-02-26 15:12:01 +00005243def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005244 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005245 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005246 auth.add_auth_options(parser)
5247 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005248 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005249 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005250 if args:
5251 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005252 cl = Changelist(auth_config=auth_config, issue=options.issue,
5253 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005254 # Ensure there actually is an issue to close.
5255 cl.GetDescription()
5256 cl.CloseIssue()
5257 return 0
5258
5259
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005260def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005261 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005262 parser.add_option(
5263 '--stat',
5264 action='store_true',
5265 dest='stat',
5266 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005267 auth.add_auth_options(parser)
5268 options, args = parser.parse_args(args)
5269 auth_config = auth.extract_auth_config_from_options(options)
5270 if args:
5271 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005272
5273 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005274 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005275 # Staged changes would be committed along with the patch from last
5276 # upload, hence counted toward the "last upload" side in the final
5277 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005278 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005279 return 1
5280
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005281 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005282 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005283 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005284 if not issue:
5285 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005286 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005287 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005288
5289 # Create a new branch based on the merge-base
5290 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005291 # Clear cached branch in cl object, to avoid overwriting original CL branch
5292 # properties.
5293 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005294 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005295 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005296 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005297 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005298 return rtn
5299
wychen@chromium.org06928532015-02-03 02:11:29 +00005300 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005301 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005302 cmd = ['git', 'diff']
5303 if options.stat:
5304 cmd.append('--stat')
5305 cmd.extend([TMP_BRANCH, branch, '--'])
5306 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005307 finally:
5308 RunGit(['checkout', '-q', branch])
5309 RunGit(['branch', '-D', TMP_BRANCH])
5310
5311 return 0
5312
5313
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005314def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005315 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005316 parser.add_option(
5317 '--no-color',
5318 action='store_true',
5319 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005320 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005321 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005322 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005323
5324 author = RunGit(['config', 'user.email']).strip() or None
5325
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005326 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005327
5328 if args:
5329 if len(args) > 1:
5330 parser.error('Unknown args')
5331 base_branch = args[0]
5332 else:
5333 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005334 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005335
5336 change = cl.GetChange(base_branch, None)
5337 return owners_finder.OwnersFinder(
5338 [f.LocalPath() for f in
5339 cl.GetChange(base_branch, None).AffectedFiles()],
5340 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005341 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005342 disable_color=options.no_color).run()
5343
5344
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005345def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005346 """Generates a diff command."""
5347 # Generate diff for the current branch's changes.
5348 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005349 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005350
5351 if args:
5352 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005353 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005354 diff_cmd.append(arg)
5355 else:
5356 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005357
5358 return diff_cmd
5359
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005360
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005361def MatchingFileType(file_name, extensions):
5362 """Returns true if the file name ends with one of the given extensions."""
5363 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005364
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005365
enne@chromium.org555cfe42014-01-29 18:21:39 +00005366@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005367def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005368 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005369 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005370 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005371 parser.add_option('--full', action='store_true',
5372 help='Reformat the full content of all touched files')
5373 parser.add_option('--dry-run', action='store_true',
5374 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005375 parser.add_option('--python', action='store_true',
5376 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005377 parser.add_option('--js', action='store_true',
5378 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005379 parser.add_option('--diff', action='store_true',
5380 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005381 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005382
Daniel Chengc55eecf2016-12-30 03:11:02 -08005383 # Normalize any remaining args against the current path, so paths relative to
5384 # the current directory are still resolved as expected.
5385 args = [os.path.join(os.getcwd(), arg) for arg in args]
5386
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005387 # git diff generates paths against the root of the repository. Change
5388 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005389 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005390 if rel_base_path:
5391 os.chdir(rel_base_path)
5392
digit@chromium.org29e47272013-05-17 17:01:46 +00005393 # Grab the merge-base commit, i.e. the upstream commit of the current
5394 # branch when it was created or the last time it was rebased. This is
5395 # to cover the case where the user may have called "git fetch origin",
5396 # moving the origin branch to a newer commit, but hasn't rebased yet.
5397 upstream_commit = None
5398 cl = Changelist()
5399 upstream_branch = cl.GetUpstreamBranch()
5400 if upstream_branch:
5401 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5402 upstream_commit = upstream_commit.strip()
5403
5404 if not upstream_commit:
5405 DieWithError('Could not find base commit for this branch. '
5406 'Are you in detached state?')
5407
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005408 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5409 diff_output = RunGit(changed_files_cmd)
5410 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005411 # Filter out files deleted by this CL
5412 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005413
Christopher Lamc5ba6922017-01-24 11:19:14 +11005414 if opts.js:
5415 CLANG_EXTS.append('.js')
5416
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005417 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5418 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5419 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005420 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005421
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005422 top_dir = os.path.normpath(
5423 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5424
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005425 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5426 # formatted. This is used to block during the presubmit.
5427 return_value = 0
5428
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005429 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005430 # Locate the clang-format binary in the checkout
5431 try:
5432 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005433 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005434 DieWithError(e)
5435
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005436 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005437 cmd = [clang_format_tool]
5438 if not opts.dry_run and not opts.diff:
5439 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005440 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005441 if opts.diff:
5442 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005443 else:
5444 env = os.environ.copy()
5445 env['PATH'] = str(os.path.dirname(clang_format_tool))
5446 try:
5447 script = clang_format.FindClangFormatScriptInChromiumTree(
5448 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005449 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005450 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005451
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005452 cmd = [sys.executable, script, '-p0']
5453 if not opts.dry_run and not opts.diff:
5454 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005455
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005456 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5457 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005458
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005459 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5460 if opts.diff:
5461 sys.stdout.write(stdout)
5462 if opts.dry_run and len(stdout) > 0:
5463 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005464
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005465 # Similar code to above, but using yapf on .py files rather than clang-format
5466 # on C/C++ files
5467 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005468 yapf_tool = gclient_utils.FindExecutable('yapf')
5469 if yapf_tool is None:
5470 DieWithError('yapf not found in PATH')
5471
5472 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005473 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005474 cmd = [yapf_tool]
5475 if not opts.dry_run and not opts.diff:
5476 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005477 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005478 if opts.diff:
5479 sys.stdout.write(stdout)
5480 else:
5481 # TODO(sbc): yapf --lines mode still has some issues.
5482 # https://github.com/google/yapf/issues/154
5483 DieWithError('--python currently only works with --full')
5484
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005485 # Dart's formatter does not have the nice property of only operating on
5486 # modified chunks, so hard code full.
5487 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005488 try:
5489 command = [dart_format.FindDartFmtToolInChromiumTree()]
5490 if not opts.dry_run and not opts.diff:
5491 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005492 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005493
ppi@chromium.org6593d932016-03-03 15:41:15 +00005494 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005495 if opts.dry_run and stdout:
5496 return_value = 2
5497 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005498 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5499 'found in this checkout. Files in other languages are still '
5500 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005501
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005502 # Format GN build files. Always run on full build files for canonical form.
5503 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005504 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005505 if opts.dry_run or opts.diff:
5506 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005507 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005508 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5509 shell=sys.platform == 'win32',
5510 cwd=top_dir)
5511 if opts.dry_run and gn_ret == 2:
5512 return_value = 2 # Not formatted.
5513 elif opts.diff and gn_ret == 2:
5514 # TODO this should compute and print the actual diff.
5515 print("This change has GN build file diff for " + gn_diff_file)
5516 elif gn_ret != 0:
5517 # For non-dry run cases (and non-2 return values for dry-run), a
5518 # nonzero error code indicates a failure, probably because the file
5519 # doesn't parse.
5520 DieWithError("gn format failed on " + gn_diff_file +
5521 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005522
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005523 metrics_xml_files = [
5524 'tools/metrics/actions/actions.xml',
5525 'tools/metrics/histograms/histograms.xml',
5526 'tools/metrics/rappor/rappor.xml']
5527 for xml_file in metrics_xml_files:
5528 if xml_file in diff_files:
5529 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5530 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5531 if opts.dry_run or opts.diff:
5532 cmd.append('--diff')
5533 stdout = RunCommand(cmd, cwd=top_dir)
5534 if opts.diff:
5535 sys.stdout.write(stdout)
5536 if opts.dry_run and stdout:
5537 return_value = 2 # Not formatted.
5538
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005539 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005540
5541
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005542@subcommand.usage('<codereview url or issue id>')
5543def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005544 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005545 _, args = parser.parse_args(args)
5546
5547 if len(args) != 1:
5548 parser.print_help()
5549 return 1
5550
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005551 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005552 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005553 parser.print_help()
5554 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005555 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005556
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005557 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005558 output = RunGit(['config', '--local', '--get-regexp',
5559 r'branch\..*\.%s' % issueprefix],
5560 error_ok=True)
5561 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005562 if issue == target_issue:
5563 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005564
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005565 branches = []
5566 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005567 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005568 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005569 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005570 return 1
5571 if len(branches) == 1:
5572 RunGit(['checkout', branches[0]])
5573 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005574 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005575 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005576 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005577 which = raw_input('Choose by index: ')
5578 try:
5579 RunGit(['checkout', branches[int(which)]])
5580 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005581 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005582 return 1
5583
5584 return 0
5585
5586
maruel@chromium.org29404b52014-09-08 22:58:00 +00005587def CMDlol(parser, args):
5588 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005589 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005590 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5591 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5592 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005593 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005594 return 0
5595
5596
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005597class OptionParser(optparse.OptionParser):
5598 """Creates the option parse and add --verbose support."""
5599 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005600 optparse.OptionParser.__init__(
5601 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005602 self.add_option(
5603 '-v', '--verbose', action='count', default=0,
5604 help='Use 2 times for more debugging info')
5605
5606 def parse_args(self, args=None, values=None):
5607 options, args = optparse.OptionParser.parse_args(self, args, values)
5608 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005609 logging.basicConfig(
5610 level=levels[min(options.verbose, len(levels) - 1)],
5611 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5612 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005613 return options, args
5614
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005616def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005617 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005618 print('\nYour python version %s is unsupported, please upgrade.\n' %
5619 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005620 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005621
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005622 # Reload settings.
5623 global settings
5624 settings = Settings()
5625
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005626 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005627 dispatcher = subcommand.CommandDispatcher(__name__)
5628 try:
5629 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005630 except auth.AuthenticationError as e:
5631 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005632 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005633 if e.code != 500:
5634 raise
5635 DieWithError(
5636 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5637 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005638 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005639
5640
5641if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005642 # These affect sys.stdout so do it outside of main() to simplify mocks in
5643 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005644 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005645 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005646 try:
5647 sys.exit(main(sys.argv[1:]))
5648 except KeyboardInterrupt:
5649 sys.stderr.write('interrupted\n')
5650 sys.exit(1)