blob: 74a72739bc6c13485a9e2b7a656f745a9fd6c7db [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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 try:
tandrii5d48c322016-08-18 16:19:37 -0700122 (out, _), code = subprocess2.communicate(['git'] + args,
123 env=GetNoGitPagerEnv(),
124 stdout=subprocess2.PIPE,
125 stderr=stderr)
126 return code, out
127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
129 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
tandrii5d48c322016-08-18 16:19:37 -0700159def _git_branch_config_key(branch, key):
160 """Helper method to return Git config key for a branch."""
161 assert branch, 'branch name is required to set git config for it'
162 return 'branch.%s.%s' % (branch, key)
163
164
165def _git_get_branch_config_value(key, default=None, value_type=str,
166 branch=False):
167 """Returns git config value of given or current branch if any.
168
169 Returns default in all other cases.
170 """
171 assert value_type in (int, str, bool)
172 if branch is False: # Distinguishing default arg value from None.
173 branch = GetCurrentBranch()
174
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000175 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700176 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000177
tandrii5d48c322016-08-18 16:19:37 -0700178 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700179 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700180 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700181 # git config also has --int, but apparently git config suffers from integer
182 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700183 args.append(_git_branch_config_key(branch, key))
184 code, out = RunGitWithCode(args)
185 if code == 0:
186 value = out.strip()
187 if value_type == int:
188 return int(value)
189 if value_type == bool:
190 return bool(value.lower() == 'true')
191 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 return default
193
194
tandrii5d48c322016-08-18 16:19:37 -0700195def _git_set_branch_config_value(key, value, branch=None, **kwargs):
196 """Sets the value or unsets if it's None of a git branch config.
197
198 Valid, though not necessarily existing, branch must be provided,
199 otherwise currently checked out branch is used.
200 """
201 if not branch:
202 branch = GetCurrentBranch()
203 assert branch, 'a branch name OR currently checked out branch is required'
204 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700205 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700206 if value is None:
207 args.append('--unset')
208 elif isinstance(value, bool):
209 args.append('--bool')
210 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700211 else:
tandrii33a46ff2016-08-23 05:53:40 -0700212 # git config also has --int, but apparently git config suffers from integer
213 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700214 value = str(value)
215 args.append(_git_branch_config_key(branch, key))
216 if value is not None:
217 args.append(value)
218 RunGit(args, **kwargs)
219
220
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000221def add_git_similarity(parser):
222 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700223 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000224 help='Sets the percentage that a pair of files need to match in order to'
225 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000226 parser.add_option(
227 '--find-copies', action='store_true',
228 help='Allows git to look for copies.')
229 parser.add_option(
230 '--no-find-copies', action='store_false', dest='find_copies',
231 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000232
233 old_parser_args = parser.parse_args
234 def Parse(args):
235 options, args = old_parser_args(args)
236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000237 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700238 options.similarity = _git_get_branch_config_value(
239 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000240 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241 print('Note: Saving similarity of %d%% in git config.'
242 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700243 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 options.similarity = max(0, min(options.similarity, 100))
246
247 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.find_copies = _git_get_branch_config_value(
249 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 else:
tandrii5d48c322016-08-18 16:19:37 -0700251 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000252
253 print('Using %d%% similarity for rename/copy detection. '
254 'Override with --similarity.' % options.similarity)
255
256 return options, args
257 parser.parse_args = Parse
258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
261 properties = dict(x.split('=', 1) for x in options.properties)
262 for key, val in properties.iteritems():
263 try:
264 properties[key] = json.loads(val)
265 except ValueError:
266 pass # If a value couldn't be evaluated, treat it as a string.
267 return properties
268
269
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270def _prefix_master(master):
271 """Convert user-specified master name to full master name.
272
273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
274 name, while the developers always use shortened master name
275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
276 function does the conversion for buildbucket migration.
277 """
278 prefix = 'master.'
279 if master.startswith(prefix):
280 return master
281 return '%s%s' % (prefix, master)
282
283
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000284def _buildbucket_retry(operation_name, http, *args, **kwargs):
285 """Retries requests to buildbucket service and returns parsed json content."""
286 try_count = 0
287 while True:
288 response, content = http.request(*args, **kwargs)
289 try:
290 content_json = json.loads(content)
291 except ValueError:
292 content_json = None
293
294 # Buildbucket could return an error even if status==200.
295 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000296 error = content_json.get('error')
297 if error.get('code') == 403:
298 raise BuildbucketResponseException(
299 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000300 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000301 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000302 raise BuildbucketResponseException(msg)
303
304 if response.status == 200:
305 if not content_json:
306 raise BuildbucketResponseException(
307 'Buildbucket returns invalid json content: %s.\n'
308 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
309 content)
310 return content_json
311 if response.status < 500 or try_count >= 2:
312 raise httplib2.HttpLib2Error(content)
313
314 # status >= 500 means transient failures.
315 logging.debug('Transient errors when %s. Will retry.', operation_name)
316 time.sleep(0.5 + 1.5*try_count)
317 try_count += 1
318 assert False, 'unreachable'
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322 rietveld_url = settings.GetDefaultServerUrl()
323 rietveld_host = urlparse.urlparse(rietveld_url).hostname
324 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
325 http = authenticator.authorize(httplib2.Http())
326 http.force_exception_to_status_code = True
327 issue_props = changelist.GetIssueProperties()
328 issue = changelist.GetIssue()
329 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000330 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332 buildbucket_put_url = (
333 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000334 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
336 hostname=rietveld_host,
337 issue=issue,
338 patch=patchset)
339
340 batch_req_body = {'builds': []}
341 print_text = []
342 print_text.append('Tried jobs on:')
343 for master, builders_and_tests in sorted(masters.iteritems()):
344 print_text.append('Master: %s' % master)
345 bucket = _prefix_master(master)
346 for builder, tests in sorted(builders_and_tests.iteritems()):
347 print_text.append(' %s: %s' % (builder, tests))
348 parameters = {
349 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000350 'changes': [{
351 'author': {'email': issue_props['owner_email']},
352 'revision': options.revision,
353 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'properties': {
355 'category': category,
356 'issue': issue,
357 'master': master,
358 'patch_project': issue_props['project'],
359 'patch_storage': 'rietveld',
360 'patchset': patchset,
361 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 },
364 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000365 if 'presubmit' in builder.lower():
366 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000367 if tests:
368 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000369 if properties:
370 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 if options.clobber:
372 parameters['properties']['clobber'] = True
373 batch_req_body['builds'].append(
374 {
375 'bucket': bucket,
376 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000378 'tags': ['builder:%s' % builder,
379 'buildset:%s' % buildset,
380 'master:%s' % master,
381 'user_agent:git_cl_try']
382 }
383 )
384
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700386 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 http,
388 buildbucket_put_url,
389 'PUT',
390 body=json.dumps(batch_req_body),
391 headers={'Content-Type': 'application/json'}
392 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000393 print_text.append('To see results here, run: git cl try-results')
394 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700395 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
tandrii221ab252016-10-06 08:12:04 -0700398def fetch_try_jobs(auth_config, changelist, buildbucket_host,
399 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700400 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401
qyearsley53f48a12016-09-01 10:45:13 -0700402 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 """
tandrii221ab252016-10-06 08:12:04 -0700404 assert buildbucket_host
405 assert changelist.GetIssue(), 'CL must be uploaded first'
406 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
407 patchset = patchset or changelist.GetMostRecentPatchset()
408 assert patchset, 'CL must be uploaded first'
409
410 codereview_url = changelist.GetCodereviewServer()
411 codereview_host = urlparse.urlparse(codereview_url).hostname
412 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000413 if authenticator.has_cached_credentials():
414 http = authenticator.authorize(httplib2.Http())
415 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('Warning: Some results might be missing because %s' %
417 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700418 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 http = httplib2.Http()
420
421 http.force_exception_to_status_code = True
422
tandrii221ab252016-10-06 08:12:04 -0700423 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
424 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
425 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000426 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700427 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000428 params = {'tag': 'buildset:%s' % buildset}
429
430 builds = {}
431 while True:
432 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700433 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000434 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700435 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000436 for build in content.get('builds', []):
437 builds[build['id']] = build
438 if 'next_cursor' in content:
439 params['start_cursor'] = content['next_cursor']
440 else:
441 break
442 return builds
443
444
qyearsleyeab3c042016-08-24 09:18:28 -0700445def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000446 """Prints nicely result of fetch_try_jobs."""
447 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700448 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000449 return
450
451 # Make a copy, because we'll be modifying builds dictionary.
452 builds = builds.copy()
453 builder_names_cache = {}
454
455 def get_builder(b):
456 try:
457 return builder_names_cache[b['id']]
458 except KeyError:
459 try:
460 parameters = json.loads(b['parameters_json'])
461 name = parameters['builder_name']
462 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700463 print('WARNING: failed to get builder name for build %s: %s' % (
464 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000465 name = None
466 builder_names_cache[b['id']] = name
467 return name
468
469 def get_bucket(b):
470 bucket = b['bucket']
471 if bucket.startswith('master.'):
472 return bucket[len('master.'):]
473 return bucket
474
475 if options.print_master:
476 name_fmt = '%%-%ds %%-%ds' % (
477 max(len(str(get_bucket(b))) for b in builds.itervalues()),
478 max(len(str(get_builder(b))) for b in builds.itervalues()))
479 def get_name(b):
480 return name_fmt % (get_bucket(b), get_builder(b))
481 else:
482 name_fmt = '%%-%ds' % (
483 max(len(str(get_builder(b))) for b in builds.itervalues()))
484 def get_name(b):
485 return name_fmt % get_builder(b)
486
487 def sort_key(b):
488 return b['status'], b.get('result'), get_name(b), b.get('url')
489
490 def pop(title, f, color=None, **kwargs):
491 """Pop matching builds from `builds` dict and print them."""
492
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000493 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 colorize = str
495 else:
496 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
497
498 result = []
499 for b in builds.values():
500 if all(b.get(k) == v for k, v in kwargs.iteritems()):
501 builds.pop(b['id'])
502 result.append(b)
503 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700504 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700506 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507
508 total = len(builds)
509 pop(status='COMPLETED', result='SUCCESS',
510 title='Successes:', color=Fore.GREEN,
511 f=lambda b: (get_name(b), b.get('url')))
512 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
513 title='Infra Failures:', color=Fore.MAGENTA,
514 f=lambda b: (get_name(b), b.get('url')))
515 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
516 title='Failures:', color=Fore.RED,
517 f=lambda b: (get_name(b), b.get('url')))
518 pop(status='COMPLETED', result='CANCELED',
519 title='Canceled:', color=Fore.MAGENTA,
520 f=lambda b: (get_name(b),))
521 pop(status='COMPLETED', result='FAILURE',
522 failure_reason='INVALID_BUILD_DEFINITION',
523 title='Wrong master/builder name:', color=Fore.MAGENTA,
524 f=lambda b: (get_name(b),))
525 pop(status='COMPLETED', result='FAILURE',
526 title='Other failures:',
527 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
528 pop(status='COMPLETED',
529 title='Other finished:',
530 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
531 pop(status='STARTED',
532 title='Started:', color=Fore.YELLOW,
533 f=lambda b: (get_name(b), b.get('url')))
534 pop(status='SCHEDULED',
535 title='Scheduled:',
536 f=lambda b: (get_name(b), 'id=%s' % b['id']))
537 # The last section is just in case buildbucket API changes OR there is a bug.
538 pop(title='Other:',
539 f=lambda b: (get_name(b), 'id=%s' % b['id']))
540 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700541 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542
543
qyearsley53f48a12016-09-01 10:45:13 -0700544def write_try_results_json(output_file, builds):
545 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
546
547 The input |builds| dict is assumed to be generated by Buildbucket.
548 Buildbucket documentation: http://goo.gl/G0s101
549 """
550
551 def convert_build_dict(build):
552 return {
553 'buildbucket_id': build.get('id'),
554 'status': build.get('status'),
555 'result': build.get('result'),
556 'bucket': build.get('bucket'),
557 'builder_name': json.loads(
558 build.get('parameters_json', '{}')).get('builder_name'),
559 'failure_reason': build.get('failure_reason'),
560 'url': build.get('url'),
561 }
562
563 converted = []
564 for _, build in sorted(builds.items()):
565 converted.append(convert_build_dict(build))
566 write_json(output_file, converted)
567
568
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000569def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
570 """Return the corresponding git ref if |base_url| together with |glob_spec|
571 matches the full |url|.
572
573 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
574 """
575 fetch_suburl, as_ref = glob_spec.split(':')
576 if allow_wildcards:
577 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
578 if glob_match:
579 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
580 # "branches/{472,597,648}/src:refs/remotes/svn/*".
581 branch_re = re.escape(base_url)
582 if glob_match.group(1):
583 branch_re += '/' + re.escape(glob_match.group(1))
584 wildcard = glob_match.group(2)
585 if wildcard == '*':
586 branch_re += '([^/]*)'
587 else:
588 # Escape and replace surrounding braces with parentheses and commas
589 # with pipe symbols.
590 wildcard = re.escape(wildcard)
591 wildcard = re.sub('^\\\\{', '(', wildcard)
592 wildcard = re.sub('\\\\,', '|', wildcard)
593 wildcard = re.sub('\\\\}$', ')', wildcard)
594 branch_re += wildcard
595 if glob_match.group(3):
596 branch_re += re.escape(glob_match.group(3))
597 match = re.match(branch_re, url)
598 if match:
599 return re.sub('\*$', match.group(1), as_ref)
600
601 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
602 if fetch_suburl:
603 full_url = base_url + '/' + fetch_suburl
604 else:
605 full_url = base_url
606 if full_url == url:
607 return as_ref
608 return None
609
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000610
iannucci@chromium.org79540052012-10-19 23:15:26 +0000611def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000612 """Prints statistics about the change to the user."""
613 # --no-ext-diff is broken in some versions of Git, so try to work around
614 # this by overriding the environment (but there is still a problem if the
615 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000616 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000617 if 'GIT_EXTERNAL_DIFF' in env:
618 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000619
620 if find_copies:
621 similarity_options = ['--find-copies-harder', '-l100000',
622 '-C%s' % similarity]
623 else:
624 similarity_options = ['-M%s' % similarity]
625
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000626 try:
627 stdout = sys.stdout.fileno()
628 except AttributeError:
629 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000630 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000631 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000632 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000633 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000634
635
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000636class BuildbucketResponseException(Exception):
637 pass
638
639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640class Settings(object):
641 def __init__(self):
642 self.default_server = None
643 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000644 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 self.is_git_svn = None
646 self.svn_branch = None
647 self.tree_status_url = None
648 self.viewvc_url = None
649 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000650 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000651 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000652 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000653 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000654 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000655 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000656 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700657 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658
659 def LazyUpdateIfNeeded(self):
660 """Updates the settings from a codereview.settings file, if available."""
661 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000662 # The only value that actually changes the behavior is
663 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000664 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000665 error_ok=True
666 ).strip().lower()
667
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000669 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 LoadCodereviewSettingsFromFile(cr_settings_file)
671 self.updated = True
672
673 def GetDefaultServerUrl(self, error_ok=False):
674 if not self.default_server:
675 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000676 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000677 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 if error_ok:
679 return self.default_server
680 if not self.default_server:
681 error_message = ('Could not find settings file. You must configure '
682 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000683 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000684 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 return self.default_server
686
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000687 @staticmethod
688 def GetRelativeRoot():
689 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000692 if self.root is None:
693 self.root = os.path.abspath(self.GetRelativeRoot())
694 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000696 def GetGitMirror(self, remote='origin'):
697 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000698 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000699 if not os.path.isdir(local_url):
700 return None
701 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
702 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
703 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
704 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
705 if mirror.exists():
706 return mirror
707 return None
708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 def GetIsGitSvn(self):
710 """Return true if this repo looks like it's using git-svn."""
711 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000712 if self.GetPendingRefPrefix():
713 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
714 self.is_git_svn = False
715 else:
716 # If you have any "svn-remote.*" config keys, we think you're using svn.
717 self.is_git_svn = RunGitWithCode(
718 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 return self.is_git_svn
720
721 def GetSVNBranch(self):
722 if self.svn_branch is None:
723 if not self.GetIsGitSvn():
724 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
725
726 # Try to figure out which remote branch we're based on.
727 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000728 # 1) iterate through our branch history and find the svn URL.
729 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730
731 # regexp matching the git-svn line that contains the URL.
732 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
733
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000734 # We don't want to go through all of history, so read a line from the
735 # pipe at a time.
736 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000737 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
739 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000740 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000741 for line in proc.stdout:
742 match = git_svn_re.match(line)
743 if match:
744 url = match.group(1)
745 proc.stdout.close() # Cut pipe.
746 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000748 if url:
749 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
750 remotes = RunGit(['config', '--get-regexp',
751 r'^svn-remote\..*\.url']).splitlines()
752 for remote in remotes:
753 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000755 remote = match.group(1)
756 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000757 rewrite_root = RunGit(
758 ['config', 'svn-remote.%s.rewriteRoot' % remote],
759 error_ok=True).strip()
760 if rewrite_root:
761 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000762 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000763 ['config', 'svn-remote.%s.fetch' % remote],
764 error_ok=True).strip()
765 if fetch_spec:
766 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
767 if self.svn_branch:
768 break
769 branch_spec = RunGit(
770 ['config', 'svn-remote.%s.branches' % remote],
771 error_ok=True).strip()
772 if branch_spec:
773 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
774 if self.svn_branch:
775 break
776 tag_spec = RunGit(
777 ['config', 'svn-remote.%s.tags' % remote],
778 error_ok=True).strip()
779 if tag_spec:
780 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
781 if self.svn_branch:
782 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
784 if not self.svn_branch:
785 DieWithError('Can\'t guess svn branch -- try specifying it on the '
786 'command line')
787
788 return self.svn_branch
789
790 def GetTreeStatusUrl(self, error_ok=False):
791 if not self.tree_status_url:
792 error_message = ('You must configure your tree status URL by running '
793 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self.tree_status_url = self._GetRietveldConfig(
795 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 return self.tree_status_url
797
798 def GetViewVCUrl(self):
799 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return self.viewvc_url
802
rmistry@google.com90752582014-01-14 21:04:50 +0000803 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000805
rmistry@google.com78948ed2015-07-08 23:09:57 +0000806 def GetIsSkipDependencyUpload(self, branch_name):
807 """Returns true if specified branch should skip dep uploads."""
808 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
809 error_ok=True)
810
rmistry@google.com5626a922015-02-26 14:03:30 +0000811 def GetRunPostUploadHook(self):
812 run_post_upload_hook = self._GetRietveldConfig(
813 'run-post-upload-hook', error_ok=True)
814 return run_post_upload_hook == "True"
815
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000816 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000818
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000819 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000820 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000821
ukai@chromium.orge8077812012-02-03 03:41:46 +0000822 def GetIsGerrit(self):
823 """Return true if this repo is assosiated with gerrit code review system."""
824 if self.is_gerrit is None:
825 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
826 return self.is_gerrit
827
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000828 def GetSquashGerritUploads(self):
829 """Return true if uploads to Gerrit should be squashed by default."""
830 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700831 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
832 if self.squash_gerrit_uploads is None:
833 # Default is squash now (http://crbug.com/611892#c23).
834 self.squash_gerrit_uploads = not (
835 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
836 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000837 return self.squash_gerrit_uploads
838
tandriia60502f2016-06-20 02:01:53 -0700839 def GetSquashGerritUploadsOverride(self):
840 """Return True or False if codereview.settings should be overridden.
841
842 Returns None if no override has been defined.
843 """
844 # See also http://crbug.com/611892#c23
845 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
846 error_ok=True).strip()
847 if result == 'true':
848 return True
849 if result == 'false':
850 return False
851 return None
852
tandrii@chromium.org28253532016-04-14 13:46:56 +0000853 def GetGerritSkipEnsureAuthenticated(self):
854 """Return True if EnsureAuthenticated should not be done for Gerrit
855 uploads."""
856 if self.gerrit_skip_ensure_authenticated is None:
857 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000858 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000859 error_ok=True).strip() == 'true')
860 return self.gerrit_skip_ensure_authenticated
861
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000862 def GetGitEditor(self):
863 """Return the editor specified in the git config, or None if none is."""
864 if self.git_editor is None:
865 self.git_editor = self._GetConfig('core.editor', error_ok=True)
866 return self.git_editor or None
867
thestig@chromium.org44202a22014-03-11 19:22:18 +0000868 def GetLintRegex(self):
869 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
870 DEFAULT_LINT_REGEX)
871
872 def GetLintIgnoreRegex(self):
873 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
874 DEFAULT_LINT_IGNORE_REGEX)
875
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000876 def GetProject(self):
877 if not self.project:
878 self.project = self._GetRietveldConfig('project', error_ok=True)
879 return self.project
880
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000881 def GetForceHttpsCommitUrl(self):
882 if not self.force_https_commit_url:
883 self.force_https_commit_url = self._GetRietveldConfig(
884 'force-https-commit-url', error_ok=True)
885 return self.force_https_commit_url
886
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000887 def GetPendingRefPrefix(self):
888 if not self.pending_ref_prefix:
889 self.pending_ref_prefix = self._GetRietveldConfig(
890 'pending-ref-prefix', error_ok=True)
891 return self.pending_ref_prefix
892
tandriif46c20f2016-09-14 06:17:05 -0700893 def GetHasGitNumberFooter(self):
894 # TODO(tandrii): this has to be removed after Rietveld is read-only.
895 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
896 if not self.git_number_footer:
897 self.git_number_footer = self._GetRietveldConfig(
898 'git-number-footer', error_ok=True)
899 return self.git_number_footer
900
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000901 def _GetRietveldConfig(self, param, **kwargs):
902 return self._GetConfig('rietveld.' + param, **kwargs)
903
rmistry@google.com78948ed2015-07-08 23:09:57 +0000904 def _GetBranchConfig(self, branch_name, param, **kwargs):
905 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
906
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 def _GetConfig(self, param, **kwargs):
908 self.LazyUpdateIfNeeded()
909 return RunGit(['config', param], **kwargs).strip()
910
911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912def ShortBranchName(branch):
913 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000914 return branch.replace('refs/heads/', '', 1)
915
916
917def GetCurrentBranchRef():
918 """Returns branch ref (e.g., refs/heads/master) or None."""
919 return RunGit(['symbolic-ref', 'HEAD'],
920 stderr=subprocess2.VOID, error_ok=True).strip() or None
921
922
923def GetCurrentBranch():
924 """Returns current branch or None.
925
926 For refs/heads/* branches, returns just last part. For others, full ref.
927 """
928 branchref = GetCurrentBranchRef()
929 if branchref:
930 return ShortBranchName(branchref)
931 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000934class _CQState(object):
935 """Enum for states of CL with respect to Commit Queue."""
936 NONE = 'none'
937 DRY_RUN = 'dry_run'
938 COMMIT = 'commit'
939
940 ALL_STATES = [NONE, DRY_RUN, COMMIT]
941
942
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000943class _ParsedIssueNumberArgument(object):
944 def __init__(self, issue=None, patchset=None, hostname=None):
945 self.issue = issue
946 self.patchset = patchset
947 self.hostname = hostname
948
949 @property
950 def valid(self):
951 return self.issue is not None
952
953
954class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
955 def __init__(self, *args, **kwargs):
956 self.patch_url = kwargs.pop('patch_url', None)
957 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
958
959
960def ParseIssueNumberArgument(arg):
961 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
962 fail_result = _ParsedIssueNumberArgument()
963
964 if arg.isdigit():
965 return _ParsedIssueNumberArgument(issue=int(arg))
966 if not arg.startswith('http'):
967 return fail_result
968 url = gclient_utils.UpgradeToHttps(arg)
969 try:
970 parsed_url = urlparse.urlparse(url)
971 except ValueError:
972 return fail_result
973 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
974 tmp = cls.ParseIssueURL(parsed_url)
975 if tmp is not None:
976 return tmp
977 return fail_result
978
979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000981 """Changelist works with one changelist in local branch.
982
983 Supports two codereview backends: Rietveld or Gerrit, selected at object
984 creation.
985
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000986 Notes:
987 * Not safe for concurrent multi-{thread,process} use.
988 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700989 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000990 """
991
992 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
993 """Create a new ChangeList instance.
994
995 If issue is given, the codereview must be given too.
996
997 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
998 Otherwise, it's decided based on current configuration of the local branch,
999 with default being 'rietveld' for backwards compatibility.
1000 See _load_codereview_impl for more details.
1001
1002 **kwargs will be passed directly to codereview implementation.
1003 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001005 global settings
1006 if not settings:
1007 # Happens when git_cl.py is used as a utility library.
1008 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009
1010 if issue:
1011 assert codereview, 'codereview must be known, if issue is known'
1012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 self.branchref = branchref
1014 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001015 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 self.branch = ShortBranchName(self.branchref)
1017 else:
1018 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001020 self.lookedup_issue = False
1021 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 self.has_description = False
1023 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001024 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001026 self.cc = None
1027 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001028 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001029
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001030 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001031 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001032 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001033 assert self._codereview_impl
1034 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001035
1036 def _load_codereview_impl(self, codereview=None, **kwargs):
1037 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001038 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1039 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1040 self._codereview = codereview
1041 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001042 return
1043
1044 # Automatic selection based on issue number set for a current branch.
1045 # Rietveld takes precedence over Gerrit.
1046 assert not self.issue
1047 # Whether we find issue or not, we are doing the lookup.
1048 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001049 if self.GetBranch():
1050 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1051 issue = _git_get_branch_config_value(
1052 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1053 if issue:
1054 self._codereview = codereview
1055 self._codereview_impl = cls(self, **kwargs)
1056 self.issue = int(issue)
1057 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001058
1059 # No issue is set for this branch, so decide based on repo-wide settings.
1060 return self._load_codereview_impl(
1061 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1062 **kwargs)
1063
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001064 def IsGerrit(self):
1065 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001066
1067 def GetCCList(self):
1068 """Return the users cc'd on this CL.
1069
agable92bec4f2016-08-24 09:27:27 -07001070 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001071 """
1072 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001073 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001074 more_cc = ','.join(self.watchers)
1075 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1076 return self.cc
1077
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001078 def GetCCListWithoutDefault(self):
1079 """Return the users cc'd on this CL excluding default ones."""
1080 if self.cc is None:
1081 self.cc = ','.join(self.watchers)
1082 return self.cc
1083
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001084 def SetWatchers(self, watchers):
1085 """Set the list of email addresses that should be cc'd based on the changed
1086 files in this CL.
1087 """
1088 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089
1090 def GetBranch(self):
1091 """Returns the short branch name, e.g. 'master'."""
1092 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001094 if not branchref:
1095 return None
1096 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.branch = ShortBranchName(self.branchref)
1098 return self.branch
1099
1100 def GetBranchRef(self):
1101 """Returns the full branch name, e.g. 'refs/heads/master'."""
1102 self.GetBranch() # Poke the lazy loader.
1103 return self.branchref
1104
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001105 def ClearBranch(self):
1106 """Clears cached branch data of this object."""
1107 self.branch = self.branchref = None
1108
tandrii5d48c322016-08-18 16:19:37 -07001109 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1110 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1111 kwargs['branch'] = self.GetBranch()
1112 return _git_get_branch_config_value(key, default, **kwargs)
1113
1114 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1115 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1116 assert self.GetBranch(), (
1117 'this CL must have an associated branch to %sset %s%s' %
1118 ('un' if value is None else '',
1119 key,
1120 '' if value is None else ' to %r' % value))
1121 kwargs['branch'] = self.GetBranch()
1122 return _git_set_branch_config_value(key, value, **kwargs)
1123
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001124 @staticmethod
1125 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001126 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 e.g. 'origin', 'refs/heads/master'
1128 """
1129 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001130 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001133 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001135 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1136 error_ok=True).strip()
1137 if upstream_branch:
1138 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001140 # Fall back on trying a git-svn upstream branch.
1141 if settings.GetIsGitSvn():
1142 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001144 # Else, try to guess the origin remote.
1145 remote_branches = RunGit(['branch', '-r']).split()
1146 if 'origin/master' in remote_branches:
1147 # Fall back on origin/master if it exits.
1148 remote = 'origin'
1149 upstream_branch = 'refs/heads/master'
1150 elif 'origin/trunk' in remote_branches:
1151 # Fall back on origin/trunk if it exists. Generally a shared
1152 # git-svn clone
1153 remote = 'origin'
1154 upstream_branch = 'refs/heads/trunk'
1155 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 DieWithError(
1157 'Unable to determine default branch to diff against.\n'
1158 'Either pass complete "git diff"-style arguments, like\n'
1159 ' git cl upload origin/master\n'
1160 'or verify this branch is set up to track another \n'
1161 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162
1163 return remote, upstream_branch
1164
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001165 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001166 upstream_branch = self.GetUpstreamBranch()
1167 if not BranchExists(upstream_branch):
1168 DieWithError('The upstream for the current branch (%s) does not exist '
1169 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001170 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001171 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 def GetUpstreamBranch(self):
1174 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001175 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001177 upstream_branch = upstream_branch.replace('refs/heads/',
1178 'refs/remotes/%s/' % remote)
1179 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1180 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 self.upstream_branch = upstream_branch
1182 return self.upstream_branch
1183
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001184 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001185 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001186 remote, branch = None, self.GetBranch()
1187 seen_branches = set()
1188 while branch not in seen_branches:
1189 seen_branches.add(branch)
1190 remote, branch = self.FetchUpstreamTuple(branch)
1191 branch = ShortBranchName(branch)
1192 if remote != '.' or branch.startswith('refs/remotes'):
1193 break
1194 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001195 remotes = RunGit(['remote'], error_ok=True).split()
1196 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001197 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001198 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001199 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001200 logging.warning('Could not determine which remote this change is '
1201 'associated with, so defaulting to "%s". This may '
1202 'not be what you want. You may prevent this message '
1203 'by running "git svn info" as documented here: %s',
1204 self._remote,
1205 GIT_INSTRUCTIONS_URL)
1206 else:
1207 logging.warn('Could not determine which remote this change is '
1208 'associated with. You may prevent this message by '
1209 'running "git svn info" as documented here: %s',
1210 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001211 branch = 'HEAD'
1212 if branch.startswith('refs/remotes'):
1213 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001214 elif branch.startswith('refs/branch-heads/'):
1215 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001216 else:
1217 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001218 return self._remote
1219
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 def GitSanityChecks(self, upstream_git_obj):
1221 """Checks git repo status and ensures diff is from local commits."""
1222
sbc@chromium.org79706062015-01-14 21:18:12 +00001223 if upstream_git_obj is None:
1224 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001225 print('ERROR: unable to determine current branch (detached HEAD?)',
1226 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001227 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001228 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001229 return False
1230
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 # Verify the commit we're diffing against is in our current branch.
1232 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1233 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1234 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001235 print('ERROR: %s is not in the current branch. You may need to rebase '
1236 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 return False
1238
1239 # List the commits inside the diff, and verify they are all local.
1240 commits_in_diff = RunGit(
1241 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1242 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1243 remote_branch = remote_branch.strip()
1244 if code != 0:
1245 _, remote_branch = self.GetRemoteBranch()
1246
1247 commits_in_remote = RunGit(
1248 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1249
1250 common_commits = set(commits_in_diff) & set(commits_in_remote)
1251 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001252 print('ERROR: Your diff contains %d commits already in %s.\n'
1253 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1254 'the diff. If you are using a custom git flow, you can override'
1255 ' the reference used for this check with "git config '
1256 'gitcl.remotebranch <git-ref>".' % (
1257 len(common_commits), remote_branch, upstream_git_obj),
1258 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001259 return False
1260 return True
1261
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001262 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001263 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001264
1265 Returns None if it is not set.
1266 """
tandrii5d48c322016-08-18 16:19:37 -07001267 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001268
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001269 def GetGitSvnRemoteUrl(self):
1270 """Return the configured git-svn remote URL parsed from git svn info.
1271
1272 Returns None if it is not set.
1273 """
1274 # URL is dependent on the current directory.
1275 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1276 if data:
1277 keys = dict(line.split(': ', 1) for line in data.splitlines()
1278 if ': ' in line)
1279 return keys.get('URL', None)
1280 return None
1281
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 def GetRemoteUrl(self):
1283 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1284
1285 Returns None if there is no remote.
1286 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001288 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1289
1290 # If URL is pointing to a local directory, it is probably a git cache.
1291 if os.path.isdir(url):
1292 url = RunGit(['config', 'remote.%s.url' % remote],
1293 error_ok=True,
1294 cwd=url).strip()
1295 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001297 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001298 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001299 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001300 self.issue = self._GitGetBranchConfigValue(
1301 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001302 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 return self.issue
1304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 def GetIssueURL(self):
1306 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001307 issue = self.GetIssue()
1308 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001309 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311
1312 def GetDescription(self, pretty=False):
1313 if not self.has_description:
1314 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001315 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 self.has_description = True
1317 if pretty:
1318 wrapper = textwrap.TextWrapper()
1319 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1320 return wrapper.fill(self.description)
1321 return self.description
1322
1323 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001324 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001325 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001326 self.patchset = self._GitGetBranchConfigValue(
1327 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001328 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 return self.patchset
1330
1331 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001332 """Set this branch's patchset. If patchset=0, clears the patchset."""
1333 assert self.GetBranch()
1334 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001335 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001336 else:
1337 self.patchset = int(patchset)
1338 self._GitSetBranchConfigValue(
1339 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001341 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001342 """Set this branch's issue. If issue isn't given, clears the issue."""
1343 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001345 issue = int(issue)
1346 self._GitSetBranchConfigValue(
1347 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001349 codereview_server = self._codereview_impl.GetCodereviewServer()
1350 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001351 self._GitSetBranchConfigValue(
1352 self._codereview_impl.CodereviewServerConfigKey(),
1353 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 else:
tandrii5d48c322016-08-18 16:19:37 -07001355 # Reset all of these just to be clean.
1356 reset_suffixes = [
1357 'last-upload-hash',
1358 self._codereview_impl.IssueConfigKey(),
1359 self._codereview_impl.PatchsetConfigKey(),
1360 self._codereview_impl.CodereviewServerConfigKey(),
1361 ] + self._PostUnsetIssueProperties()
1362 for prop in reset_suffixes:
1363 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001364 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001365 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
dnjba1b0f32016-09-02 12:37:42 -07001367 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001368 if not self.GitSanityChecks(upstream_branch):
1369 DieWithError('\nGit sanity check failure')
1370
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001371 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001372 if not root:
1373 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001374 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001375
1376 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001377 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001378 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001379 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001380 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001381 except subprocess2.CalledProcessError:
1382 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001383 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001384 'This branch probably doesn\'t exist anymore. To reset the\n'
1385 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001386 ' git branch --set-upstream-to origin/master %s\n'
1387 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001388 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001389
maruel@chromium.org52424302012-08-29 15:14:30 +00001390 issue = self.GetIssue()
1391 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001392 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001393 description = self.GetDescription()
1394 else:
1395 # If the change was never uploaded, use the log messages of all commits
1396 # up to the branch point, as git cl upload will prefill the description
1397 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001398 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1399 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001400
1401 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001402 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001403 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001404 name,
1405 description,
1406 absroot,
1407 files,
1408 issue,
1409 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001410 author,
1411 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001412
dsansomee2d6fd92016-09-08 00:10:47 -07001413 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001414 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001415 return self._codereview_impl.UpdateDescriptionRemote(
1416 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001417
1418 def RunHook(self, committing, may_prompt, verbose, change):
1419 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1420 try:
1421 return presubmit_support.DoPresubmitChecks(change, committing,
1422 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1423 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001424 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1425 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001426 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001427 DieWithError(
1428 ('%s\nMaybe your depot_tools is out of date?\n'
1429 'If all fails, contact maruel@') % e)
1430
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001431 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1432 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001433 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1434 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001435 else:
1436 # Assume url.
1437 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1438 urlparse.urlparse(issue_arg))
1439 if not parsed_issue_arg or not parsed_issue_arg.valid:
1440 DieWithError('Failed to parse issue argument "%s". '
1441 'Must be an issue number or a valid URL.' % issue_arg)
1442 return self._codereview_impl.CMDPatchWithParsedIssue(
1443 parsed_issue_arg, reject, nocommit, directory)
1444
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001445 def CMDUpload(self, options, git_diff_args, orig_args):
1446 """Uploads a change to codereview."""
1447 if git_diff_args:
1448 # TODO(ukai): is it ok for gerrit case?
1449 base_branch = git_diff_args[0]
1450 else:
1451 if self.GetBranch() is None:
1452 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1453
1454 # Default to diffing against common ancestor of upstream branch
1455 base_branch = self.GetCommonAncestorWithUpstream()
1456 git_diff_args = [base_branch, 'HEAD']
1457
1458 # Make sure authenticated to codereview before running potentially expensive
1459 # hooks. It is a fast, best efforts check. Codereview still can reject the
1460 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001461 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001462
1463 # Apply watchlists on upload.
1464 change = self.GetChange(base_branch, None)
1465 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1466 files = [f.LocalPath() for f in change.AffectedFiles()]
1467 if not options.bypass_watchlists:
1468 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1469
1470 if not options.bypass_hooks:
1471 if options.reviewers or options.tbr_owners:
1472 # Set the reviewer list now so that presubmit checks can access it.
1473 change_description = ChangeDescription(change.FullDescriptionText())
1474 change_description.update_reviewers(options.reviewers,
1475 options.tbr_owners,
1476 change)
1477 change.SetDescriptionText(change_description.description)
1478 hook_results = self.RunHook(committing=False,
1479 may_prompt=not options.force,
1480 verbose=options.verbose,
1481 change=change)
1482 if not hook_results.should_continue():
1483 return 1
1484 if not options.reviewers and hook_results.reviewers:
1485 options.reviewers = hook_results.reviewers.split(',')
1486
1487 if self.GetIssue():
1488 latest_patchset = self.GetMostRecentPatchset()
1489 local_patchset = self.GetPatchset()
1490 if (latest_patchset and local_patchset and
1491 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001492 print('The last upload made from this repository was patchset #%d but '
1493 'the most recent patchset on the server is #%d.'
1494 % (local_patchset, latest_patchset))
1495 print('Uploading will still work, but if you\'ve uploaded to this '
1496 'issue from another machine or branch the patch you\'re '
1497 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001498 ask_for_data('About to upload; enter to confirm.')
1499
1500 print_stats(options.similarity, options.find_copies, git_diff_args)
1501 ret = self.CMDUploadChange(options, git_diff_args, change)
1502 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001503 if options.use_commit_queue:
1504 self.SetCQState(_CQState.COMMIT)
1505 elif options.cq_dry_run:
1506 self.SetCQState(_CQState.DRY_RUN)
1507
tandrii5d48c322016-08-18 16:19:37 -07001508 _git_set_branch_config_value('last-upload-hash',
1509 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001510 # Run post upload hooks, if specified.
1511 if settings.GetRunPostUploadHook():
1512 presubmit_support.DoPostUploadExecuter(
1513 change,
1514 self,
1515 settings.GetRoot(),
1516 options.verbose,
1517 sys.stdout)
1518
1519 # Upload all dependencies if specified.
1520 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001521 print()
1522 print('--dependencies has been specified.')
1523 print('All dependent local branches will be re-uploaded.')
1524 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001525 # Remove the dependencies flag from args so that we do not end up in a
1526 # loop.
1527 orig_args.remove('--dependencies')
1528 ret = upload_branch_deps(self, orig_args)
1529 return ret
1530
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001531 def SetCQState(self, new_state):
1532 """Update the CQ state for latest patchset.
1533
1534 Issue must have been already uploaded and known.
1535 """
1536 assert new_state in _CQState.ALL_STATES
1537 assert self.GetIssue()
1538 return self._codereview_impl.SetCQState(new_state)
1539
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001540 # Forward methods to codereview specific implementation.
1541
1542 def CloseIssue(self):
1543 return self._codereview_impl.CloseIssue()
1544
1545 def GetStatus(self):
1546 return self._codereview_impl.GetStatus()
1547
1548 def GetCodereviewServer(self):
1549 return self._codereview_impl.GetCodereviewServer()
1550
1551 def GetApprovingReviewers(self):
1552 return self._codereview_impl.GetApprovingReviewers()
1553
1554 def GetMostRecentPatchset(self):
1555 return self._codereview_impl.GetMostRecentPatchset()
1556
1557 def __getattr__(self, attr):
1558 # This is because lots of untested code accesses Rietveld-specific stuff
1559 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001560 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001561 # Note that child method defines __getattr__ as well, and forwards it here,
1562 # because _RietveldChangelistImpl is not cleaned up yet, and given
1563 # deprecation of Rietveld, it should probably be just removed.
1564 # Until that time, avoid infinite recursion by bypassing __getattr__
1565 # of implementation class.
1566 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567
1568
1569class _ChangelistCodereviewBase(object):
1570 """Abstract base class encapsulating codereview specifics of a changelist."""
1571 def __init__(self, changelist):
1572 self._changelist = changelist # instance of Changelist
1573
1574 def __getattr__(self, attr):
1575 # Forward methods to changelist.
1576 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1577 # _RietveldChangelistImpl to avoid this hack?
1578 return getattr(self._changelist, attr)
1579
1580 def GetStatus(self):
1581 """Apply a rough heuristic to give a simple summary of an issue's review
1582 or CQ status, assuming adherence to a common workflow.
1583
1584 Returns None if no issue for this branch, or specific string keywords.
1585 """
1586 raise NotImplementedError()
1587
1588 def GetCodereviewServer(self):
1589 """Returns server URL without end slash, like "https://codereview.com"."""
1590 raise NotImplementedError()
1591
1592 def FetchDescription(self):
1593 """Fetches and returns description from the codereview server."""
1594 raise NotImplementedError()
1595
tandrii5d48c322016-08-18 16:19:37 -07001596 @classmethod
1597 def IssueConfigKey(cls):
1598 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001599 raise NotImplementedError()
1600
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001601 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001602 def PatchsetConfigKey(cls):
1603 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001604 raise NotImplementedError()
1605
tandrii5d48c322016-08-18 16:19:37 -07001606 @classmethod
1607 def CodereviewServerConfigKey(cls):
1608 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001609 raise NotImplementedError()
1610
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001611 def _PostUnsetIssueProperties(self):
1612 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001613 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001614
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 def GetRieveldObjForPresubmit(self):
1616 # This is an unfortunate Rietveld-embeddedness in presubmit.
1617 # For non-Rietveld codereviews, this probably should return a dummy object.
1618 raise NotImplementedError()
1619
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001620 def GetGerritObjForPresubmit(self):
1621 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1622 return None
1623
dsansomee2d6fd92016-09-08 00:10:47 -07001624 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001625 """Update the description on codereview site."""
1626 raise NotImplementedError()
1627
1628 def CloseIssue(self):
1629 """Closes the issue."""
1630 raise NotImplementedError()
1631
1632 def GetApprovingReviewers(self):
1633 """Returns a list of reviewers approving the change.
1634
1635 Note: not necessarily committers.
1636 """
1637 raise NotImplementedError()
1638
1639 def GetMostRecentPatchset(self):
1640 """Returns the most recent patchset number from the codereview site."""
1641 raise NotImplementedError()
1642
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001643 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1644 directory):
1645 """Fetches and applies the issue.
1646
1647 Arguments:
1648 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1649 reject: if True, reject the failed patch instead of switching to 3-way
1650 merge. Rietveld only.
1651 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1652 only.
1653 directory: switch to directory before applying the patch. Rietveld only.
1654 """
1655 raise NotImplementedError()
1656
1657 @staticmethod
1658 def ParseIssueURL(parsed_url):
1659 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1660 failed."""
1661 raise NotImplementedError()
1662
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001663 def EnsureAuthenticated(self, force):
1664 """Best effort check that user is authenticated with codereview server.
1665
1666 Arguments:
1667 force: whether to skip confirmation questions.
1668 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 raise NotImplementedError()
1670
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001671 def CMDUploadChange(self, options, args, change):
1672 """Uploads a change to codereview."""
1673 raise NotImplementedError()
1674
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001675 def SetCQState(self, new_state):
1676 """Update the CQ state for latest patchset.
1677
1678 Issue must have been already uploaded and known.
1679 """
1680 raise NotImplementedError()
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682
1683class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1684 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1685 super(_RietveldChangelistImpl, self).__init__(changelist)
1686 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001687 if not rietveld_server:
1688 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689
1690 self._rietveld_server = rietveld_server
1691 self._auth_config = auth_config
1692 self._props = None
1693 self._rpc_server = None
1694
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001695 def GetCodereviewServer(self):
1696 if not self._rietveld_server:
1697 # If we're on a branch then get the server potentially associated
1698 # with that branch.
1699 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001700 self._rietveld_server = gclient_utils.UpgradeToHttps(
1701 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 if not self._rietveld_server:
1703 self._rietveld_server = settings.GetDefaultServerUrl()
1704 return self._rietveld_server
1705
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001706 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001707 """Best effort check that user is authenticated with Rietveld server."""
1708 if self._auth_config.use_oauth2:
1709 authenticator = auth.get_authenticator_for_host(
1710 self.GetCodereviewServer(), self._auth_config)
1711 if not authenticator.has_cached_credentials():
1712 raise auth.LoginRequiredError(self.GetCodereviewServer())
1713
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001714 def FetchDescription(self):
1715 issue = self.GetIssue()
1716 assert issue
1717 try:
1718 return self.RpcServer().get_description(issue).strip()
1719 except urllib2.HTTPError as e:
1720 if e.code == 404:
1721 DieWithError(
1722 ('\nWhile fetching the description for issue %d, received a '
1723 '404 (not found)\n'
1724 'error. It is likely that you deleted this '
1725 'issue on the server. If this is the\n'
1726 'case, please run\n\n'
1727 ' git cl issue 0\n\n'
1728 'to clear the association with the deleted issue. Then run '
1729 'this command again.') % issue)
1730 else:
1731 DieWithError(
1732 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1733 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001734 print('Warning: Failed to retrieve CL description due to network '
1735 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 return ''
1737
1738 def GetMostRecentPatchset(self):
1739 return self.GetIssueProperties()['patchsets'][-1]
1740
1741 def GetPatchSetDiff(self, issue, patchset):
1742 return self.RpcServer().get(
1743 '/download/issue%s_%s.diff' % (issue, patchset))
1744
1745 def GetIssueProperties(self):
1746 if self._props is None:
1747 issue = self.GetIssue()
1748 if not issue:
1749 self._props = {}
1750 else:
1751 self._props = self.RpcServer().get_issue_properties(issue, True)
1752 return self._props
1753
1754 def GetApprovingReviewers(self):
1755 return get_approving_reviewers(self.GetIssueProperties())
1756
1757 def AddComment(self, message):
1758 return self.RpcServer().add_comment(self.GetIssue(), message)
1759
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001760 def GetStatus(self):
1761 """Apply a rough heuristic to give a simple summary of an issue's review
1762 or CQ status, assuming adherence to a common workflow.
1763
1764 Returns None if no issue for this branch, or one of the following keywords:
1765 * 'error' - error from review tool (including deleted issues)
1766 * 'unsent' - not sent for review
1767 * 'waiting' - waiting for review
1768 * 'reply' - waiting for owner to reply to review
1769 * 'lgtm' - LGTM from at least one approved reviewer
1770 * 'commit' - in the commit queue
1771 * 'closed' - closed
1772 """
1773 if not self.GetIssue():
1774 return None
1775
1776 try:
1777 props = self.GetIssueProperties()
1778 except urllib2.HTTPError:
1779 return 'error'
1780
1781 if props.get('closed'):
1782 # Issue is closed.
1783 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001784 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001785 # Issue is in the commit queue.
1786 return 'commit'
1787
1788 try:
1789 reviewers = self.GetApprovingReviewers()
1790 except urllib2.HTTPError:
1791 return 'error'
1792
1793 if reviewers:
1794 # Was LGTM'ed.
1795 return 'lgtm'
1796
1797 messages = props.get('messages') or []
1798
tandrii9d2c7a32016-06-22 03:42:45 -07001799 # Skip CQ messages that don't require owner's action.
1800 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1801 if 'Dry run:' in messages[-1]['text']:
1802 messages.pop()
1803 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1804 # This message always follows prior messages from CQ,
1805 # so skip this too.
1806 messages.pop()
1807 else:
1808 # This is probably a CQ messages warranting user attention.
1809 break
1810
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001811 if not messages:
1812 # No message was sent.
1813 return 'unsent'
1814 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001815 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001816 return 'reply'
1817 return 'waiting'
1818
dsansomee2d6fd92016-09-08 00:10:47 -07001819 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001820 return self.RpcServer().update_description(
1821 self.GetIssue(), self.description)
1822
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001823 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001824 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001826 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001827 return self.SetFlags({flag: value})
1828
1829 def SetFlags(self, flags):
1830 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001831 """
phajdan.jr68598232016-08-10 03:28:28 -07001832 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001833 try:
tandrii4b233bd2016-07-06 03:50:29 -07001834 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001835 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001836 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001837 if e.code == 404:
1838 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1839 if e.code == 403:
1840 DieWithError(
1841 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001842 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001843 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001845 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 """Returns an upload.RpcServer() to access this review's rietveld instance.
1847 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001848 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001849 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001850 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001851 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001852 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001854 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001855 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001856 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857
tandrii5d48c322016-08-18 16:19:37 -07001858 @classmethod
1859 def PatchsetConfigKey(cls):
1860 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861
tandrii5d48c322016-08-18 16:19:37 -07001862 @classmethod
1863 def CodereviewServerConfigKey(cls):
1864 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001865
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866 def GetRieveldObjForPresubmit(self):
1867 return self.RpcServer()
1868
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001869 def SetCQState(self, new_state):
1870 props = self.GetIssueProperties()
1871 if props.get('private'):
1872 DieWithError('Cannot set-commit on private issue')
1873
1874 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001875 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001876 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001877 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001878 else:
tandrii4b233bd2016-07-06 03:50:29 -07001879 assert new_state == _CQState.DRY_RUN
1880 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001881
1882
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001883 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1884 directory):
1885 # TODO(maruel): Use apply_issue.py
1886
1887 # PatchIssue should never be called with a dirty tree. It is up to the
1888 # caller to check this, but just in case we assert here since the
1889 # consequences of the caller not checking this could be dire.
1890 assert(not git_common.is_dirty_git_tree('apply'))
1891 assert(parsed_issue_arg.valid)
1892 self._changelist.issue = parsed_issue_arg.issue
1893 if parsed_issue_arg.hostname:
1894 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1895
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001896 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1897 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001898 assert parsed_issue_arg.patchset
1899 patchset = parsed_issue_arg.patchset
1900 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1901 else:
1902 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1903 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1904
1905 # Switch up to the top-level directory, if necessary, in preparation for
1906 # applying the patch.
1907 top = settings.GetRelativeRoot()
1908 if top:
1909 os.chdir(top)
1910
1911 # Git patches have a/ at the beginning of source paths. We strip that out
1912 # with a sed script rather than the -p flag to patch so we can feed either
1913 # Git or svn-style patches into the same apply command.
1914 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1915 try:
1916 patch_data = subprocess2.check_output(
1917 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1918 except subprocess2.CalledProcessError:
1919 DieWithError('Git patch mungling failed.')
1920 logging.info(patch_data)
1921
1922 # We use "git apply" to apply the patch instead of "patch" so that we can
1923 # pick up file adds.
1924 # The --index flag means: also insert into the index (so we catch adds).
1925 cmd = ['git', 'apply', '--index', '-p0']
1926 if directory:
1927 cmd.extend(('--directory', directory))
1928 if reject:
1929 cmd.append('--reject')
1930 elif IsGitVersionAtLeast('1.7.12'):
1931 cmd.append('--3way')
1932 try:
1933 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1934 stdin=patch_data, stdout=subprocess2.VOID)
1935 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001936 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001937 return 1
1938
1939 # If we had an issue, commit the current state and register the issue.
1940 if not nocommit:
1941 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1942 'patch from issue %(i)s at patchset '
1943 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1944 % {'i': self.GetIssue(), 'p': patchset})])
1945 self.SetIssue(self.GetIssue())
1946 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001947 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001948 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001949 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001950 return 0
1951
1952 @staticmethod
1953 def ParseIssueURL(parsed_url):
1954 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1955 return None
wychen3c1c1722016-08-04 11:46:36 -07001956 # Rietveld patch: https://domain/<number>/#ps<patchset>
1957 match = re.match(r'/(\d+)/$', parsed_url.path)
1958 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1959 if match and match2:
1960 return _RietveldParsedIssueNumberArgument(
1961 issue=int(match.group(1)),
1962 patchset=int(match2.group(1)),
1963 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001964 # Typical url: https://domain/<issue_number>[/[other]]
1965 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1966 if match:
1967 return _RietveldParsedIssueNumberArgument(
1968 issue=int(match.group(1)),
1969 hostname=parsed_url.netloc)
1970 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1971 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1972 if match:
1973 return _RietveldParsedIssueNumberArgument(
1974 issue=int(match.group(1)),
1975 patchset=int(match.group(2)),
1976 hostname=parsed_url.netloc,
1977 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1978 return None
1979
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001980 def CMDUploadChange(self, options, args, change):
1981 """Upload the patch to Rietveld."""
1982 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1983 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001984 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1985 if options.emulate_svn_auto_props:
1986 upload_args.append('--emulate_svn_auto_props')
1987
1988 change_desc = None
1989
1990 if options.email is not None:
1991 upload_args.extend(['--email', options.email])
1992
1993 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001994 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 upload_args.extend(['--title', options.title])
1996 if options.message:
1997 upload_args.extend(['--message', options.message])
1998 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001999 print('This branch is associated with issue %s. '
2000 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002001 else:
nodirca166002016-06-27 10:59:51 -07002002 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002003 upload_args.extend(['--title', options.title])
2004 message = (options.title or options.message or
2005 CreateDescriptionFromLog(args))
2006 change_desc = ChangeDescription(message)
2007 if options.reviewers or options.tbr_owners:
2008 change_desc.update_reviewers(options.reviewers,
2009 options.tbr_owners,
2010 change)
2011 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002012 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002013
2014 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002015 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002016 return 1
2017
2018 upload_args.extend(['--message', change_desc.description])
2019 if change_desc.get_reviewers():
2020 upload_args.append('--reviewers=%s' % ','.join(
2021 change_desc.get_reviewers()))
2022 if options.send_mail:
2023 if not change_desc.get_reviewers():
2024 DieWithError("Must specify reviewers to send email.")
2025 upload_args.append('--send_mail')
2026
2027 # We check this before applying rietveld.private assuming that in
2028 # rietveld.cc only addresses which we can send private CLs to are listed
2029 # if rietveld.private is set, and so we should ignore rietveld.cc only
2030 # when --private is specified explicitly on the command line.
2031 if options.private:
2032 logging.warn('rietveld.cc is ignored since private flag is specified. '
2033 'You need to review and add them manually if necessary.')
2034 cc = self.GetCCListWithoutDefault()
2035 else:
2036 cc = self.GetCCList()
2037 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2038 if cc:
2039 upload_args.extend(['--cc', cc])
2040
2041 if options.private or settings.GetDefaultPrivateFlag() == "True":
2042 upload_args.append('--private')
2043
2044 upload_args.extend(['--git_similarity', str(options.similarity)])
2045 if not options.find_copies:
2046 upload_args.extend(['--git_no_find_copies'])
2047
2048 # Include the upstream repo's URL in the change -- this is useful for
2049 # projects that have their source spread across multiple repos.
2050 remote_url = self.GetGitBaseUrlFromConfig()
2051 if not remote_url:
2052 if settings.GetIsGitSvn():
2053 remote_url = self.GetGitSvnRemoteUrl()
2054 else:
2055 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2056 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2057 self.GetUpstreamBranch().split('/')[-1])
2058 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002059 remote, remote_branch = self.GetRemoteBranch()
2060 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2061 settings.GetPendingRefPrefix())
2062 if target_ref:
2063 upload_args.extend(['--target_ref', target_ref])
2064
2065 # Look for dependent patchsets. See crbug.com/480453 for more details.
2066 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2067 upstream_branch = ShortBranchName(upstream_branch)
2068 if remote is '.':
2069 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002070 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002071 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002072 print()
2073 print('Skipping dependency patchset upload because git config '
2074 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2075 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002076 else:
2077 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002078 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002079 auth_config=auth_config)
2080 branch_cl_issue_url = branch_cl.GetIssueURL()
2081 branch_cl_issue = branch_cl.GetIssue()
2082 branch_cl_patchset = branch_cl.GetPatchset()
2083 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2084 upload_args.extend(
2085 ['--depends_on_patchset', '%s:%s' % (
2086 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002087 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002088 '\n'
2089 'The current branch (%s) is tracking a local branch (%s) with '
2090 'an associated CL.\n'
2091 'Adding %s/#ps%s as a dependency patchset.\n'
2092 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2093 branch_cl_patchset))
2094
2095 project = settings.GetProject()
2096 if project:
2097 upload_args.extend(['--project', project])
2098
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002099 try:
2100 upload_args = ['upload'] + upload_args + args
2101 logging.info('upload.RealMain(%s)', upload_args)
2102 issue, patchset = upload.RealMain(upload_args)
2103 issue = int(issue)
2104 patchset = int(patchset)
2105 except KeyboardInterrupt:
2106 sys.exit(1)
2107 except:
2108 # If we got an exception after the user typed a description for their
2109 # change, back up the description before re-raising.
2110 if change_desc:
2111 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2112 print('\nGot exception while uploading -- saving description to %s\n' %
2113 backup_path)
2114 backup_file = open(backup_path, 'w')
2115 backup_file.write(change_desc.description)
2116 backup_file.close()
2117 raise
2118
2119 if not self.GetIssue():
2120 self.SetIssue(issue)
2121 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122 return 0
2123
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002124
2125class _GerritChangelistImpl(_ChangelistCodereviewBase):
2126 def __init__(self, changelist, auth_config=None):
2127 # auth_config is Rietveld thing, kept here to preserve interface only.
2128 super(_GerritChangelistImpl, self).__init__(changelist)
2129 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002130 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002131 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002132 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002133
2134 def _GetGerritHost(self):
2135 # Lazy load of configs.
2136 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002137 if self._gerrit_host and '.' not in self._gerrit_host:
2138 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2139 # This happens for internal stuff http://crbug.com/614312.
2140 parsed = urlparse.urlparse(self.GetRemoteUrl())
2141 if parsed.scheme == 'sso':
2142 print('WARNING: using non https URLs for remote is likely broken\n'
2143 ' Your current remote is: %s' % self.GetRemoteUrl())
2144 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2145 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002146 return self._gerrit_host
2147
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002148 def _GetGitHost(self):
2149 """Returns git host to be used when uploading change to Gerrit."""
2150 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2151
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002152 def GetCodereviewServer(self):
2153 if not self._gerrit_server:
2154 # If we're on a branch then get the server potentially associated
2155 # with that branch.
2156 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002157 self._gerrit_server = self._GitGetBranchConfigValue(
2158 self.CodereviewServerConfigKey())
2159 if self._gerrit_server:
2160 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002161 if not self._gerrit_server:
2162 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2163 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002164 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002165 parts[0] = parts[0] + '-review'
2166 self._gerrit_host = '.'.join(parts)
2167 self._gerrit_server = 'https://%s' % self._gerrit_host
2168 return self._gerrit_server
2169
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002170 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002171 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002172 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002173
tandrii5d48c322016-08-18 16:19:37 -07002174 @classmethod
2175 def PatchsetConfigKey(cls):
2176 return 'gerritpatchset'
2177
2178 @classmethod
2179 def CodereviewServerConfigKey(cls):
2180 return 'gerritserver'
2181
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002182 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002183 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002184 if settings.GetGerritSkipEnsureAuthenticated():
2185 # For projects with unusual authentication schemes.
2186 # See http://crbug.com/603378.
2187 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002188 # Lazy-loader to identify Gerrit and Git hosts.
2189 if gerrit_util.GceAuthenticator.is_gce():
2190 return
2191 self.GetCodereviewServer()
2192 git_host = self._GetGitHost()
2193 assert self._gerrit_server and self._gerrit_host
2194 cookie_auth = gerrit_util.CookiesAuthenticator()
2195
2196 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2197 git_auth = cookie_auth.get_auth_header(git_host)
2198 if gerrit_auth and git_auth:
2199 if gerrit_auth == git_auth:
2200 return
2201 print((
2202 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2203 ' Check your %s or %s file for credentials of hosts:\n'
2204 ' %s\n'
2205 ' %s\n'
2206 ' %s') %
2207 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2208 git_host, self._gerrit_host,
2209 cookie_auth.get_new_password_message(git_host)))
2210 if not force:
2211 ask_for_data('If you know what you are doing, press Enter to continue, '
2212 'Ctrl+C to abort.')
2213 return
2214 else:
2215 missing = (
2216 [] if gerrit_auth else [self._gerrit_host] +
2217 [] if git_auth else [git_host])
2218 DieWithError('Credentials for the following hosts are required:\n'
2219 ' %s\n'
2220 'These are read from %s (or legacy %s)\n'
2221 '%s' % (
2222 '\n '.join(missing),
2223 cookie_auth.get_gitcookies_path(),
2224 cookie_auth.get_netrc_path(),
2225 cookie_auth.get_new_password_message(git_host)))
2226
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002227 def _PostUnsetIssueProperties(self):
2228 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002229 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002230
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002231 def GetRieveldObjForPresubmit(self):
2232 class ThisIsNotRietveldIssue(object):
2233 def __nonzero__(self):
2234 # This is a hack to make presubmit_support think that rietveld is not
2235 # defined, yet still ensure that calls directly result in a decent
2236 # exception message below.
2237 return False
2238
2239 def __getattr__(self, attr):
2240 print(
2241 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2242 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2243 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2244 'or use Rietveld for codereview.\n'
2245 'See also http://crbug.com/579160.' % attr)
2246 raise NotImplementedError()
2247 return ThisIsNotRietveldIssue()
2248
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002249 def GetGerritObjForPresubmit(self):
2250 return presubmit_support.GerritAccessor(self._GetGerritHost())
2251
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002252 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002253 """Apply a rough heuristic to give a simple summary of an issue's review
2254 or CQ status, assuming adherence to a common workflow.
2255
2256 Returns None if no issue for this branch, or one of the following keywords:
2257 * 'error' - error from review tool (including deleted issues)
2258 * 'unsent' - no reviewers added
2259 * 'waiting' - waiting for review
2260 * 'reply' - waiting for owner to reply to review
2261 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2262 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2263 * 'commit' - in the commit queue
2264 * 'closed' - abandoned
2265 """
2266 if not self.GetIssue():
2267 return None
2268
2269 try:
2270 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2271 except httplib.HTTPException:
2272 return 'error'
2273
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002274 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002275 return 'closed'
2276
2277 cq_label = data['labels'].get('Commit-Queue', {})
2278 if cq_label:
2279 # Vote value is a stringified integer, which we expect from 0 to 2.
2280 vote_value = cq_label.get('value', '0')
2281 vote_text = cq_label.get('values', {}).get(vote_value, '')
2282 if vote_text.lower() == 'commit':
2283 return 'commit'
2284
2285 lgtm_label = data['labels'].get('Code-Review', {})
2286 if lgtm_label:
2287 if 'rejected' in lgtm_label:
2288 return 'not lgtm'
2289 if 'approved' in lgtm_label:
2290 return 'lgtm'
2291
2292 if not data.get('reviewers', {}).get('REVIEWER', []):
2293 return 'unsent'
2294
2295 messages = data.get('messages', [])
2296 if messages:
2297 owner = data['owner'].get('_account_id')
2298 last_message_author = messages[-1].get('author', {}).get('_account_id')
2299 if owner != last_message_author:
2300 # Some reply from non-owner.
2301 return 'reply'
2302
2303 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002304
2305 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002306 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307 return data['revisions'][data['current_revision']]['_number']
2308
2309 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002310 data = self._GetChangeDetail(['CURRENT_REVISION'])
2311 current_rev = data['current_revision']
2312 url = data['revisions'][current_rev]['fetch']['http']['url']
2313 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002314
dsansomee2d6fd92016-09-08 00:10:47 -07002315 def UpdateDescriptionRemote(self, description, force=False):
2316 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2317 if not force:
2318 ask_for_data(
2319 'The description cannot be modified while the issue has a pending '
2320 'unpublished edit. Either publish the edit in the Gerrit web UI '
2321 'or delete it.\n\n'
2322 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2323
2324 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2325 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002326 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2327 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002328
2329 def CloseIssue(self):
2330 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2331
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002332 def GetApprovingReviewers(self):
2333 """Returns a list of reviewers approving the change.
2334
2335 Note: not necessarily committers.
2336 """
2337 raise NotImplementedError()
2338
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002339 def SubmitIssue(self, wait_for_merge=True):
2340 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2341 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002342
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002343 def _GetChangeDetail(self, options=None, issue=None):
2344 options = options or []
2345 issue = issue or self.GetIssue()
2346 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002347 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2348 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002349
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002350 def CMDLand(self, force, bypass_hooks, verbose):
2351 if git_common.is_dirty_git_tree('land'):
2352 return 1
tandriid60367b2016-06-22 05:25:12 -07002353 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2354 if u'Commit-Queue' in detail.get('labels', {}):
2355 if not force:
2356 ask_for_data('\nIt seems this repository has a Commit Queue, '
2357 'which can test and land changes for you. '
2358 'Are you sure you wish to bypass it?\n'
2359 'Press Enter to continue, Ctrl+C to abort.')
2360
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002361 differs = True
tandriic4344b52016-08-29 06:04:54 -07002362 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002363 # Note: git diff outputs nothing if there is no diff.
2364 if not last_upload or RunGit(['diff', last_upload]).strip():
2365 print('WARNING: some changes from local branch haven\'t been uploaded')
2366 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002367 if detail['current_revision'] == last_upload:
2368 differs = False
2369 else:
2370 print('WARNING: local branch contents differ from latest uploaded '
2371 'patchset')
2372 if differs:
2373 if not force:
2374 ask_for_data(
2375 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2376 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2377 elif not bypass_hooks:
2378 hook_results = self.RunHook(
2379 committing=True,
2380 may_prompt=not force,
2381 verbose=verbose,
2382 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2383 if not hook_results.should_continue():
2384 return 1
2385
2386 self.SubmitIssue(wait_for_merge=True)
2387 print('Issue %s has been submitted.' % self.GetIssueURL())
2388 return 0
2389
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002390 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2391 directory):
2392 assert not reject
2393 assert not nocommit
2394 assert not directory
2395 assert parsed_issue_arg.valid
2396
2397 self._changelist.issue = parsed_issue_arg.issue
2398
2399 if parsed_issue_arg.hostname:
2400 self._gerrit_host = parsed_issue_arg.hostname
2401 self._gerrit_server = 'https://%s' % self._gerrit_host
2402
2403 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2404
2405 if not parsed_issue_arg.patchset:
2406 # Use current revision by default.
2407 revision_info = detail['revisions'][detail['current_revision']]
2408 patchset = int(revision_info['_number'])
2409 else:
2410 patchset = parsed_issue_arg.patchset
2411 for revision_info in detail['revisions'].itervalues():
2412 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2413 break
2414 else:
2415 DieWithError('Couldn\'t find patchset %i in issue %i' %
2416 (parsed_issue_arg.patchset, self.GetIssue()))
2417
2418 fetch_info = revision_info['fetch']['http']
2419 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2420 RunGit(['cherry-pick', 'FETCH_HEAD'])
2421 self.SetIssue(self.GetIssue())
2422 self.SetPatchset(patchset)
2423 print('Committed patch for issue %i pathset %i locally' %
2424 (self.GetIssue(), self.GetPatchset()))
2425 return 0
2426
2427 @staticmethod
2428 def ParseIssueURL(parsed_url):
2429 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2430 return None
2431 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2432 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2433 # Short urls like https://domain/<issue_number> can be used, but don't allow
2434 # specifying the patchset (you'd 404), but we allow that here.
2435 if parsed_url.path == '/':
2436 part = parsed_url.fragment
2437 else:
2438 part = parsed_url.path
2439 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2440 if match:
2441 return _ParsedIssueNumberArgument(
2442 issue=int(match.group(2)),
2443 patchset=int(match.group(4)) if match.group(4) else None,
2444 hostname=parsed_url.netloc)
2445 return None
2446
tandrii16e0b4e2016-06-07 10:34:28 -07002447 def _GerritCommitMsgHookCheck(self, offer_removal):
2448 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2449 if not os.path.exists(hook):
2450 return
2451 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2452 # custom developer made one.
2453 data = gclient_utils.FileRead(hook)
2454 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2455 return
2456 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002457 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002458 'and may interfere with it in subtle ways.\n'
2459 'We recommend you remove the commit-msg hook.')
2460 if offer_removal:
2461 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2462 if reply.lower().startswith('y'):
2463 gclient_utils.rm_file_or_tree(hook)
2464 print('Gerrit commit-msg hook removed.')
2465 else:
2466 print('OK, will keep Gerrit commit-msg hook in place.')
2467
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002468 def CMDUploadChange(self, options, args, change):
2469 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002470 if options.squash and options.no_squash:
2471 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002472
2473 if not options.squash and not options.no_squash:
2474 # Load default for user, repo, squash=true, in this order.
2475 options.squash = settings.GetSquashGerritUploads()
2476 elif options.no_squash:
2477 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002478
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002479 # We assume the remote called "origin" is the one we want.
2480 # It is probably not worthwhile to support different workflows.
2481 gerrit_remote = 'origin'
2482
2483 remote, remote_branch = self.GetRemoteBranch()
2484 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2485 pending_prefix='')
2486
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002487 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002488 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002489 if self.GetIssue():
2490 # Try to get the message from a previous upload.
2491 message = self.GetDescription()
2492 if not message:
2493 DieWithError(
2494 'failed to fetch description from current Gerrit issue %d\n'
2495 '%s' % (self.GetIssue(), self.GetIssueURL()))
2496 change_id = self._GetChangeDetail()['change_id']
2497 while True:
2498 footer_change_ids = git_footers.get_footer_change_id(message)
2499 if footer_change_ids == [change_id]:
2500 break
2501 if not footer_change_ids:
2502 message = git_footers.add_footer_change_id(message, change_id)
2503 print('WARNING: appended missing Change-Id to issue description')
2504 continue
2505 # There is already a valid footer but with different or several ids.
2506 # Doing this automatically is non-trivial as we don't want to lose
2507 # existing other footers, yet we want to append just 1 desired
2508 # Change-Id. Thus, just create a new footer, but let user verify the
2509 # new description.
2510 message = '%s\n\nChange-Id: %s' % (message, change_id)
2511 print(
2512 'WARNING: issue %s has Change-Id footer(s):\n'
2513 ' %s\n'
2514 'but issue has Change-Id %s, according to Gerrit.\n'
2515 'Please, check the proposed correction to the description, '
2516 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2517 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2518 change_id))
2519 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2520 if not options.force:
2521 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002522 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002523 message = change_desc.description
2524 if not message:
2525 DieWithError("Description is empty. Aborting...")
2526 # Continue the while loop.
2527 # Sanity check of this code - we should end up with proper message
2528 # footer.
2529 assert [change_id] == git_footers.get_footer_change_id(message)
2530 change_desc = ChangeDescription(message)
2531 else:
2532 change_desc = ChangeDescription(
2533 options.message or CreateDescriptionFromLog(args))
2534 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002535 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002536 if not change_desc.description:
2537 DieWithError("Description is empty. Aborting...")
2538 message = change_desc.description
2539 change_ids = git_footers.get_footer_change_id(message)
2540 if len(change_ids) > 1:
2541 DieWithError('too many Change-Id footers, at most 1 allowed.')
2542 if not change_ids:
2543 # Generate the Change-Id automatically.
2544 message = git_footers.add_footer_change_id(
2545 message, GenerateGerritChangeId(message))
2546 change_desc.set_description(message)
2547 change_ids = git_footers.get_footer_change_id(message)
2548 assert len(change_ids) == 1
2549 change_id = change_ids[0]
2550
2551 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2552 if remote is '.':
2553 # If our upstream branch is local, we base our squashed commit on its
2554 # squashed version.
2555 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2556 # Check the squashed hash of the parent.
2557 parent = RunGit(['config',
2558 'branch.%s.gerritsquashhash' % upstream_branch_name],
2559 error_ok=True).strip()
2560 # Verify that the upstream branch has been uploaded too, otherwise
2561 # Gerrit will create additional CLs when uploading.
2562 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2563 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002564 DieWithError(
2565 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002566 'Note: maybe you\'ve uploaded it with --no-squash. '
2567 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002568 ' git cl upload --squash\n' % upstream_branch_name)
2569 else:
2570 parent = self.GetCommonAncestorWithUpstream()
2571
2572 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2573 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2574 '-m', message]).strip()
2575 else:
2576 change_desc = ChangeDescription(
2577 options.message or CreateDescriptionFromLog(args))
2578 if not change_desc.description:
2579 DieWithError("Description is empty. Aborting...")
2580
2581 if not git_footers.get_footer_change_id(change_desc.description):
2582 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002583 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2584 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002585 ref_to_push = 'HEAD'
2586 parent = '%s/%s' % (gerrit_remote, branch)
2587 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2588
2589 assert change_desc
2590 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2591 ref_to_push)]).splitlines()
2592 if len(commits) > 1:
2593 print('WARNING: This will upload %d commits. Run the following command '
2594 'to see which commits will be uploaded: ' % len(commits))
2595 print('git log %s..%s' % (parent, ref_to_push))
2596 print('You can also use `git squash-branch` to squash these into a '
2597 'single commit.')
2598 ask_for_data('About to upload; enter to confirm.')
2599
2600 if options.reviewers or options.tbr_owners:
2601 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2602 change)
2603
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002604 # Extra options that can be specified at push time. Doc:
2605 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2606 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002607 if change_desc.get_reviewers(tbr_only=True):
2608 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2609 refspec_opts.append('l=Code-Review+1')
2610
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002611 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002612 if not re.match(r'^[\w ]+$', options.title):
2613 options.title = re.sub(r'[^\w ]', '', options.title)
2614 print('WARNING: Patchset title may only contain alphanumeric chars '
2615 'and spaces. Cleaned up title:\n%s' % options.title)
2616 if not options.force:
2617 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002618 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2619 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002620 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2621
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002622 if options.send_mail:
2623 if not change_desc.get_reviewers():
2624 DieWithError('Must specify reviewers to send email.')
2625 refspec_opts.append('notify=ALL')
2626 else:
2627 refspec_opts.append('notify=NONE')
2628
tandrii99a72f22016-08-17 14:33:24 -07002629 reviewers = change_desc.get_reviewers()
2630 if reviewers:
2631 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002632
agablec6787972016-09-09 16:13:34 -07002633 if options.private:
2634 refspec_opts.append('draft')
2635
rmistry9eadede2016-09-19 11:22:43 -07002636 if options.topic:
2637 # Documentation on Gerrit topics is here:
2638 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2639 refspec_opts.append('topic=%s' % options.topic)
2640
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002641 refspec_suffix = ''
2642 if refspec_opts:
2643 refspec_suffix = '%' + ','.join(refspec_opts)
2644 assert ' ' not in refspec_suffix, (
2645 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002646 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002647
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002649 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002650 print_stdout=True,
2651 # Flush after every line: useful for seeing progress when running as
2652 # recipe.
2653 filter_fn=lambda _: sys.stdout.flush())
2654
2655 if options.squash:
2656 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2657 change_numbers = [m.group(1)
2658 for m in map(regex.match, push_stdout.splitlines())
2659 if m]
2660 if len(change_numbers) != 1:
2661 DieWithError(
2662 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2663 'Change-Id: %s') % (len(change_numbers), change_id))
2664 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002665 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002666
2667 # Add cc's from the CC_LIST and --cc flag (if any).
2668 cc = self.GetCCList().split(',')
2669 if options.cc:
2670 cc.extend(options.cc)
2671 cc = filter(None, [email.strip() for email in cc])
2672 if cc:
2673 gerrit_util.AddReviewers(
2674 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2675
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002676 return 0
2677
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002678 def _AddChangeIdToCommitMessage(self, options, args):
2679 """Re-commits using the current message, assumes the commit hook is in
2680 place.
2681 """
2682 log_desc = options.message or CreateDescriptionFromLog(args)
2683 git_command = ['commit', '--amend', '-m', log_desc]
2684 RunGit(git_command)
2685 new_log_desc = CreateDescriptionFromLog(args)
2686 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002687 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002688 return new_log_desc
2689 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002690 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002691
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002692 def SetCQState(self, new_state):
2693 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002694 vote_map = {
2695 _CQState.NONE: 0,
2696 _CQState.DRY_RUN: 1,
2697 _CQState.COMMIT : 2,
2698 }
2699 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2700 labels={'Commit-Queue': vote_map[new_state]})
2701
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002702
2703_CODEREVIEW_IMPLEMENTATIONS = {
2704 'rietveld': _RietveldChangelistImpl,
2705 'gerrit': _GerritChangelistImpl,
2706}
2707
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002708
iannuccie53c9352016-08-17 14:40:40 -07002709def _add_codereview_issue_select_options(parser, extra=""):
2710 _add_codereview_select_options(parser)
2711
2712 text = ('Operate on this issue number instead of the current branch\'s '
2713 'implicit issue.')
2714 if extra:
2715 text += ' '+extra
2716 parser.add_option('-i', '--issue', type=int, help=text)
2717
2718
2719def _process_codereview_issue_select_options(parser, options):
2720 _process_codereview_select_options(parser, options)
2721 if options.issue is not None and not options.forced_codereview:
2722 parser.error('--issue must be specified with either --rietveld or --gerrit')
2723
2724
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002725def _add_codereview_select_options(parser):
2726 """Appends --gerrit and --rietveld options to force specific codereview."""
2727 parser.codereview_group = optparse.OptionGroup(
2728 parser, 'EXPERIMENTAL! Codereview override options')
2729 parser.add_option_group(parser.codereview_group)
2730 parser.codereview_group.add_option(
2731 '--gerrit', action='store_true',
2732 help='Force the use of Gerrit for codereview')
2733 parser.codereview_group.add_option(
2734 '--rietveld', action='store_true',
2735 help='Force the use of Rietveld for codereview')
2736
2737
2738def _process_codereview_select_options(parser, options):
2739 if options.gerrit and options.rietveld:
2740 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2741 options.forced_codereview = None
2742 if options.gerrit:
2743 options.forced_codereview = 'gerrit'
2744 elif options.rietveld:
2745 options.forced_codereview = 'rietveld'
2746
2747
tandriif9aefb72016-07-01 09:06:51 -07002748def _get_bug_line_values(default_project, bugs):
2749 """Given default_project and comma separated list of bugs, yields bug line
2750 values.
2751
2752 Each bug can be either:
2753 * a number, which is combined with default_project
2754 * string, which is left as is.
2755
2756 This function may produce more than one line, because bugdroid expects one
2757 project per line.
2758
2759 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2760 ['v8:123', 'chromium:789']
2761 """
2762 default_bugs = []
2763 others = []
2764 for bug in bugs.split(','):
2765 bug = bug.strip()
2766 if bug:
2767 try:
2768 default_bugs.append(int(bug))
2769 except ValueError:
2770 others.append(bug)
2771
2772 if default_bugs:
2773 default_bugs = ','.join(map(str, default_bugs))
2774 if default_project:
2775 yield '%s:%s' % (default_project, default_bugs)
2776 else:
2777 yield default_bugs
2778 for other in sorted(others):
2779 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2780 yield other
2781
2782
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002783class ChangeDescription(object):
2784 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002785 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002786 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002787
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002788 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002789 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002790
agable@chromium.org42c20792013-09-12 17:34:49 +00002791 @property # www.logilab.org/ticket/89786
2792 def description(self): # pylint: disable=E0202
2793 return '\n'.join(self._description_lines)
2794
2795 def set_description(self, desc):
2796 if isinstance(desc, basestring):
2797 lines = desc.splitlines()
2798 else:
2799 lines = [line.rstrip() for line in desc]
2800 while lines and not lines[0]:
2801 lines.pop(0)
2802 while lines and not lines[-1]:
2803 lines.pop(-1)
2804 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002805
piman@chromium.org336f9122014-09-04 02:16:55 +00002806 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002807 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002808 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002809 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002810 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002811 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002812
agable@chromium.org42c20792013-09-12 17:34:49 +00002813 # Get the set of R= and TBR= lines and remove them from the desciption.
2814 regexp = re.compile(self.R_LINE)
2815 matches = [regexp.match(line) for line in self._description_lines]
2816 new_desc = [l for i, l in enumerate(self._description_lines)
2817 if not matches[i]]
2818 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002819
agable@chromium.org42c20792013-09-12 17:34:49 +00002820 # Construct new unified R= and TBR= lines.
2821 r_names = []
2822 tbr_names = []
2823 for match in matches:
2824 if not match:
2825 continue
2826 people = cleanup_list([match.group(2).strip()])
2827 if match.group(1) == 'TBR':
2828 tbr_names.extend(people)
2829 else:
2830 r_names.extend(people)
2831 for name in r_names:
2832 if name not in reviewers:
2833 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002834 if add_owners_tbr:
2835 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002836 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002837 all_reviewers = set(tbr_names + reviewers)
2838 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2839 all_reviewers)
2840 tbr_names.extend(owners_db.reviewers_for(missing_files,
2841 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002842 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2843 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2844
2845 # Put the new lines in the description where the old first R= line was.
2846 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2847 if 0 <= line_loc < len(self._description_lines):
2848 if new_tbr_line:
2849 self._description_lines.insert(line_loc, new_tbr_line)
2850 if new_r_line:
2851 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002852 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002853 if new_r_line:
2854 self.append_footer(new_r_line)
2855 if new_tbr_line:
2856 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002857
tandriif9aefb72016-07-01 09:06:51 -07002858 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002859 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002860 self.set_description([
2861 '# Enter a description of the change.',
2862 '# This will be displayed on the codereview site.',
2863 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002864 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002865 '--------------------',
2866 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002867
agable@chromium.org42c20792013-09-12 17:34:49 +00002868 regexp = re.compile(self.BUG_LINE)
2869 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002870 prefix = settings.GetBugPrefix()
2871 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2872 for value in values:
2873 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2874 self.append_footer('BUG=%s' % value)
2875
agable@chromium.org42c20792013-09-12 17:34:49 +00002876 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002877 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002878 if not content:
2879 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002880 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002881
2882 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002883 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2884 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002885 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002886 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002887
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002888 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002889 """Adds a footer line to the description.
2890
2891 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2892 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2893 that Gerrit footers are always at the end.
2894 """
2895 parsed_footer_line = git_footers.parse_footer(line)
2896 if parsed_footer_line:
2897 # Line is a gerrit footer in the form: Footer-Key: any value.
2898 # Thus, must be appended observing Gerrit footer rules.
2899 self.set_description(
2900 git_footers.add_footer(self.description,
2901 key=parsed_footer_line[0],
2902 value=parsed_footer_line[1]))
2903 return
2904
2905 if not self._description_lines:
2906 self._description_lines.append(line)
2907 return
2908
2909 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2910 if gerrit_footers:
2911 # git_footers.split_footers ensures that there is an empty line before
2912 # actual (gerrit) footers, if any. We have to keep it that way.
2913 assert top_lines and top_lines[-1] == ''
2914 top_lines, separator = top_lines[:-1], top_lines[-1:]
2915 else:
2916 separator = [] # No need for separator if there are no gerrit_footers.
2917
2918 prev_line = top_lines[-1] if top_lines else ''
2919 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2920 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2921 top_lines.append('')
2922 top_lines.append(line)
2923 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002924
tandrii99a72f22016-08-17 14:33:24 -07002925 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002926 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002927 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002928 reviewers = [match.group(2).strip()
2929 for match in matches
2930 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002931 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002932
2933
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002934def get_approving_reviewers(props):
2935 """Retrieves the reviewers that approved a CL from the issue properties with
2936 messages.
2937
2938 Note that the list may contain reviewers that are not committer, thus are not
2939 considered by the CQ.
2940 """
2941 return sorted(
2942 set(
2943 message['sender']
2944 for message in props['messages']
2945 if message['approval'] and message['sender'] in props['reviewers']
2946 )
2947 )
2948
2949
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950def FindCodereviewSettingsFile(filename='codereview.settings'):
2951 """Finds the given file starting in the cwd and going up.
2952
2953 Only looks up to the top of the repository unless an
2954 'inherit-review-settings-ok' file exists in the root of the repository.
2955 """
2956 inherit_ok_file = 'inherit-review-settings-ok'
2957 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002958 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2960 root = '/'
2961 while True:
2962 if filename in os.listdir(cwd):
2963 if os.path.isfile(os.path.join(cwd, filename)):
2964 return open(os.path.join(cwd, filename))
2965 if cwd == root:
2966 break
2967 cwd = os.path.dirname(cwd)
2968
2969
2970def LoadCodereviewSettingsFromFile(fileobj):
2971 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002972 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002974 def SetProperty(name, setting, unset_error_ok=False):
2975 fullname = 'rietveld.' + name
2976 if setting in keyvals:
2977 RunGit(['config', fullname, keyvals[setting]])
2978 else:
2979 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2980
2981 SetProperty('server', 'CODE_REVIEW_SERVER')
2982 # Only server setting is required. Other settings can be absent.
2983 # In that case, we ignore errors raised during option deletion attempt.
2984 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002985 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002986 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2987 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002988 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002989 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002990 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2991 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002992 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002993 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002994 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07002995 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002996 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2997 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002998
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002999 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003000 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003001
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003002 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003003 RunGit(['config', 'gerrit.squash-uploads',
3004 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003005
tandrii@chromium.org28253532016-04-14 13:46:56 +00003006 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003007 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003008 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3009
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003010 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3011 #should be of the form
3012 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3013 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3014 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3015 keyvals['ORIGIN_URL_CONFIG']])
3016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003017
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003018def urlretrieve(source, destination):
3019 """urllib is broken for SSL connections via a proxy therefore we
3020 can't use urllib.urlretrieve()."""
3021 with open(destination, 'w') as f:
3022 f.write(urllib2.urlopen(source).read())
3023
3024
ukai@chromium.org712d6102013-11-27 00:52:58 +00003025def hasSheBang(fname):
3026 """Checks fname is a #! script."""
3027 with open(fname) as f:
3028 return f.read(2).startswith('#!')
3029
3030
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003031# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3032def DownloadHooks(*args, **kwargs):
3033 pass
3034
3035
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003036def DownloadGerritHook(force):
3037 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003038
3039 Args:
3040 force: True to update hooks. False to install hooks if not present.
3041 """
3042 if not settings.GetIsGerrit():
3043 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003044 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003045 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3046 if not os.access(dst, os.X_OK):
3047 if os.path.exists(dst):
3048 if not force:
3049 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003050 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003051 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003052 if not hasSheBang(dst):
3053 DieWithError('Not a script: %s\n'
3054 'You need to download from\n%s\n'
3055 'into .git/hooks/commit-msg and '
3056 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003057 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3058 except Exception:
3059 if os.path.exists(dst):
3060 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003061 DieWithError('\nFailed to download hooks.\n'
3062 'You need to download from\n%s\n'
3063 'into .git/hooks/commit-msg and '
3064 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003065
3066
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003067
3068def GetRietveldCodereviewSettingsInteractively():
3069 """Prompt the user for settings."""
3070 server = settings.GetDefaultServerUrl(error_ok=True)
3071 prompt = 'Rietveld server (host[:port])'
3072 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3073 newserver = ask_for_data(prompt + ':')
3074 if not server and not newserver:
3075 newserver = DEFAULT_SERVER
3076 if newserver:
3077 newserver = gclient_utils.UpgradeToHttps(newserver)
3078 if newserver != server:
3079 RunGit(['config', 'rietveld.server', newserver])
3080
3081 def SetProperty(initial, caption, name, is_url):
3082 prompt = caption
3083 if initial:
3084 prompt += ' ("x" to clear) [%s]' % initial
3085 new_val = ask_for_data(prompt + ':')
3086 if new_val == 'x':
3087 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3088 elif new_val:
3089 if is_url:
3090 new_val = gclient_utils.UpgradeToHttps(new_val)
3091 if new_val != initial:
3092 RunGit(['config', 'rietveld.' + name, new_val])
3093
3094 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3095 SetProperty(settings.GetDefaultPrivateFlag(),
3096 'Private flag (rietveld only)', 'private', False)
3097 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3098 'tree-status-url', False)
3099 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3100 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3101 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3102 'run-post-upload-hook', False)
3103
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003104@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003106 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003107
tandrii5d0a0422016-09-14 06:24:35 -07003108 print('WARNING: git cl config works for Rietveld only')
3109 # TODO(tandrii): remove this once we switch to Gerrit.
3110 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003111 parser.add_option('--activate-update', action='store_true',
3112 help='activate auto-updating [rietveld] section in '
3113 '.git/config')
3114 parser.add_option('--deactivate-update', action='store_true',
3115 help='deactivate auto-updating [rietveld] section in '
3116 '.git/config')
3117 options, args = parser.parse_args(args)
3118
3119 if options.deactivate_update:
3120 RunGit(['config', 'rietveld.autoupdate', 'false'])
3121 return
3122
3123 if options.activate_update:
3124 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3125 return
3126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003127 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003128 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003129 return 0
3130
3131 url = args[0]
3132 if not url.endswith('codereview.settings'):
3133 url = os.path.join(url, 'codereview.settings')
3134
3135 # Load code review settings and download hooks (if available).
3136 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3137 return 0
3138
3139
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003140def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003141 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003142 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3143 branch = ShortBranchName(branchref)
3144 _, args = parser.parse_args(args)
3145 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003146 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003147 return RunGit(['config', 'branch.%s.base-url' % branch],
3148 error_ok=False).strip()
3149 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003150 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003151 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3152 error_ok=False).strip()
3153
3154
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003155def color_for_status(status):
3156 """Maps a Changelist status to color, for CMDstatus and other tools."""
3157 return {
3158 'unsent': Fore.RED,
3159 'waiting': Fore.BLUE,
3160 'reply': Fore.YELLOW,
3161 'lgtm': Fore.GREEN,
3162 'commit': Fore.MAGENTA,
3163 'closed': Fore.CYAN,
3164 'error': Fore.WHITE,
3165 }.get(status, Fore.WHITE)
3166
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003167
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003168def get_cl_statuses(changes, fine_grained, max_processes=None):
3169 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003170
3171 If fine_grained is true, this will fetch CL statuses from the server.
3172 Otherwise, simply indicate if there's a matching url for the given branches.
3173
3174 If max_processes is specified, it is used as the maximum number of processes
3175 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3176 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003177
3178 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003179 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003180 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003181 upload.verbosity = 0
3182
3183 if fine_grained:
3184 # Process one branch synchronously to work through authentication, then
3185 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003186 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003187 def fetch(cl):
3188 try:
3189 return (cl, cl.GetStatus())
3190 except:
3191 # See http://crbug.com/629863.
3192 logging.exception('failed to fetch status for %s:', cl)
3193 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003194 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003195
tandriiea9514a2016-08-17 12:32:37 -07003196 changes_to_fetch = changes[1:]
3197 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003198 # Exit early if there was only one branch to fetch.
3199 return
3200
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003201 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003202 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003203 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003204 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003205
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003206 fetched_cls = set()
3207 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003208 while True:
3209 try:
3210 row = it.next(timeout=5)
3211 except multiprocessing.TimeoutError:
3212 break
3213
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003214 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003215 yield row
3216
3217 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003218 for cl in set(changes_to_fetch) - fetched_cls:
3219 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003220
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003221 else:
3222 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003223 for cl in changes:
3224 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003225
rmistry@google.com2dd99862015-06-22 12:22:18 +00003226
3227def upload_branch_deps(cl, args):
3228 """Uploads CLs of local branches that are dependents of the current branch.
3229
3230 If the local branch dependency tree looks like:
3231 test1 -> test2.1 -> test3.1
3232 -> test3.2
3233 -> test2.2 -> test3.3
3234
3235 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3236 run on the dependent branches in this order:
3237 test2.1, test3.1, test3.2, test2.2, test3.3
3238
3239 Note: This function does not rebase your local dependent branches. Use it when
3240 you make a change to the parent branch that will not conflict with its
3241 dependent branches, and you would like their dependencies updated in
3242 Rietveld.
3243 """
3244 if git_common.is_dirty_git_tree('upload-branch-deps'):
3245 return 1
3246
3247 root_branch = cl.GetBranch()
3248 if root_branch is None:
3249 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3250 'Get on a branch!')
3251 if not cl.GetIssue() or not cl.GetPatchset():
3252 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3253 'patchset dependencies without an uploaded CL.')
3254
3255 branches = RunGit(['for-each-ref',
3256 '--format=%(refname:short) %(upstream:short)',
3257 'refs/heads'])
3258 if not branches:
3259 print('No local branches found.')
3260 return 0
3261
3262 # Create a dictionary of all local branches to the branches that are dependent
3263 # on it.
3264 tracked_to_dependents = collections.defaultdict(list)
3265 for b in branches.splitlines():
3266 tokens = b.split()
3267 if len(tokens) == 2:
3268 branch_name, tracked = tokens
3269 tracked_to_dependents[tracked].append(branch_name)
3270
vapiera7fbd5a2016-06-16 09:17:49 -07003271 print()
3272 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003273 dependents = []
3274 def traverse_dependents_preorder(branch, padding=''):
3275 dependents_to_process = tracked_to_dependents.get(branch, [])
3276 padding += ' '
3277 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003278 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003279 dependents.append(dependent)
3280 traverse_dependents_preorder(dependent, padding)
3281 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003282 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003283
3284 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003285 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003286 return 0
3287
vapiera7fbd5a2016-06-16 09:17:49 -07003288 print('This command will checkout all dependent branches and run '
3289 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003290 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3291
andybons@chromium.org962f9462016-02-03 20:00:42 +00003292 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003293 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003294 args.extend(['-t', 'Updated patchset dependency'])
3295
rmistry@google.com2dd99862015-06-22 12:22:18 +00003296 # Record all dependents that failed to upload.
3297 failures = {}
3298 # Go through all dependents, checkout the branch and upload.
3299 try:
3300 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003301 print()
3302 print('--------------------------------------')
3303 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003304 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003306 try:
3307 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003309 failures[dependent_branch] = 1
3310 except: # pylint: disable=W0702
3311 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003312 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003313 finally:
3314 # Swap back to the original root branch.
3315 RunGit(['checkout', '-q', root_branch])
3316
vapiera7fbd5a2016-06-16 09:17:49 -07003317 print()
3318 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003319 for dependent_branch in dependents:
3320 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003321 print(' %s : %s' % (dependent_branch, upload_status))
3322 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003323
3324 return 0
3325
3326
kmarshall3bff56b2016-06-06 18:31:47 -07003327def CMDarchive(parser, args):
3328 """Archives and deletes branches associated with closed changelists."""
3329 parser.add_option(
3330 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003331 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003332 parser.add_option(
3333 '-f', '--force', action='store_true',
3334 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003335 parser.add_option(
3336 '-d', '--dry-run', action='store_true',
3337 help='Skip the branch tagging and removal steps.')
3338 parser.add_option(
3339 '-t', '--notags', action='store_true',
3340 help='Do not tag archived branches. '
3341 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003342
3343 auth.add_auth_options(parser)
3344 options, args = parser.parse_args(args)
3345 if args:
3346 parser.error('Unsupported args: %s' % ' '.join(args))
3347 auth_config = auth.extract_auth_config_from_options(options)
3348
3349 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3350 if not branches:
3351 return 0
3352
vapiera7fbd5a2016-06-16 09:17:49 -07003353 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003354 changes = [Changelist(branchref=b, auth_config=auth_config)
3355 for b in branches.splitlines()]
3356 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3357 statuses = get_cl_statuses(changes,
3358 fine_grained=True,
3359 max_processes=options.maxjobs)
3360 proposal = [(cl.GetBranch(),
3361 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3362 for cl, status in statuses
3363 if status == 'closed']
3364 proposal.sort()
3365
3366 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003367 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003368 return 0
3369
3370 current_branch = GetCurrentBranch()
3371
vapiera7fbd5a2016-06-16 09:17:49 -07003372 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003373 if options.notags:
3374 for next_item in proposal:
3375 print(' ' + next_item[0])
3376 else:
3377 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3378 for next_item in proposal:
3379 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003380
kmarshall9249e012016-08-23 12:02:16 -07003381 # Quit now on precondition failure or if instructed by the user, either
3382 # via an interactive prompt or by command line flags.
3383 if options.dry_run:
3384 print('\nNo changes were made (dry run).\n')
3385 return 0
3386 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003387 print('You are currently on a branch \'%s\' which is associated with a '
3388 'closed codereview issue, so archive cannot proceed. Please '
3389 'checkout another branch and run this command again.' %
3390 current_branch)
3391 return 1
kmarshall9249e012016-08-23 12:02:16 -07003392 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003393 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3394 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003395 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003396 return 1
3397
3398 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003399 if not options.notags:
3400 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003401 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003402
vapiera7fbd5a2016-06-16 09:17:49 -07003403 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003404
3405 return 0
3406
3407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003409 """Show status of changelists.
3410
3411 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003412 - Red not sent for review or broken
3413 - Blue waiting for review
3414 - Yellow waiting for you to reply to review
3415 - Green LGTM'ed
3416 - Magenta in the commit queue
3417 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003418
3419 Also see 'git cl comments'.
3420 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003421 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003422 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003423 parser.add_option('-f', '--fast', action='store_true',
3424 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003425 parser.add_option(
3426 '-j', '--maxjobs', action='store', type=int,
3427 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003428
3429 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003430 _add_codereview_issue_select_options(
3431 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003432 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003433 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003434 if args:
3435 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003436 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003437
iannuccie53c9352016-08-17 14:40:40 -07003438 if options.issue is not None and not options.field:
3439 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003441 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003442 cl = Changelist(auth_config=auth_config, issue=options.issue,
3443 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003444 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003445 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003446 elif options.field == 'id':
3447 issueid = cl.GetIssue()
3448 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003449 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003450 elif options.field == 'patch':
3451 patchset = cl.GetPatchset()
3452 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003453 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003454 elif options.field == 'status':
3455 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003456 elif options.field == 'url':
3457 url = cl.GetIssueURL()
3458 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003459 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003460 return 0
3461
3462 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3463 if not branches:
3464 print('No local branch found.')
3465 return 0
3466
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003467 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003468 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003469 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003471 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003472 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003473 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003474
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003475 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003476 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3477 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3478 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003479 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003480 c, status = output.next()
3481 branch_statuses[c.GetBranch()] = status
3482 status = branch_statuses.pop(branch)
3483 url = cl.GetIssueURL()
3484 if url and (not status or status == 'error'):
3485 # The issue probably doesn't exist anymore.
3486 url += ' (broken)'
3487
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003488 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003489 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003490 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003491 color = ''
3492 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003493 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003494 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003495 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003496 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003497
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003498 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print()
3500 print('Current branch:',)
3501 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003502 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003503 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003504 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003505 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003506 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003507 print('Issue description:')
3508 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003509 return 0
3510
3511
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003512def colorize_CMDstatus_doc():
3513 """To be called once in main() to add colors to git cl status help."""
3514 colors = [i for i in dir(Fore) if i[0].isupper()]
3515
3516 def colorize_line(line):
3517 for color in colors:
3518 if color in line.upper():
3519 # Extract whitespaces first and the leading '-'.
3520 indent = len(line) - len(line.lstrip(' ')) + 1
3521 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3522 return line
3523
3524 lines = CMDstatus.__doc__.splitlines()
3525 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3526
3527
phajdan.jre328cf92016-08-22 04:12:17 -07003528def write_json(path, contents):
3529 with open(path, 'w') as f:
3530 json.dump(contents, f)
3531
3532
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003533@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003535 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003536
3537 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003538 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003539 parser.add_option('-r', '--reverse', action='store_true',
3540 help='Lookup the branch(es) for the specified issues. If '
3541 'no issues are specified, all branches with mapped '
3542 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003543 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003544 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003545 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003546 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547
dnj@chromium.org406c4402015-03-03 17:22:28 +00003548 if options.reverse:
3549 branches = RunGit(['for-each-ref', 'refs/heads',
3550 '--format=%(refname:short)']).splitlines()
3551
3552 # Reverse issue lookup.
3553 issue_branch_map = {}
3554 for branch in branches:
3555 cl = Changelist(branchref=branch)
3556 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3557 if not args:
3558 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003559 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003560 for issue in args:
3561 if not issue:
3562 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003563 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('Branch for issue number %s: %s' % (
3565 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003566 if options.json:
3567 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003568 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003569 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003570 if len(args) > 0:
3571 try:
3572 issue = int(args[0])
3573 except ValueError:
3574 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003575 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003576 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003577 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003578 if options.json:
3579 write_json(options.json, {
3580 'issue': cl.GetIssue(),
3581 'issue_url': cl.GetIssueURL(),
3582 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583 return 0
3584
3585
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003586def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003587 """Shows or posts review comments for any changelist."""
3588 parser.add_option('-a', '--add-comment', dest='comment',
3589 help='comment to add to an issue')
3590 parser.add_option('-i', dest='issue',
3591 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003592 parser.add_option('-j', '--json-file',
3593 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003594 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003595 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003596 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003597
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003598 issue = None
3599 if options.issue:
3600 try:
3601 issue = int(options.issue)
3602 except ValueError:
3603 DieWithError('A review issue id is expected to be a number')
3604
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003605 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003606
3607 if options.comment:
3608 cl.AddComment(options.comment)
3609 return 0
3610
3611 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003612 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003613 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003614 summary.append({
3615 'date': message['date'],
3616 'lgtm': False,
3617 'message': message['text'],
3618 'not_lgtm': False,
3619 'sender': message['sender'],
3620 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003621 if message['disapproval']:
3622 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003623 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003624 elif message['approval']:
3625 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003626 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003627 elif message['sender'] == data['owner_email']:
3628 color = Fore.MAGENTA
3629 else:
3630 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003631 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003632 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003633 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003634 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003635 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003636 if options.json_file:
3637 with open(options.json_file, 'wb') as f:
3638 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003639 return 0
3640
3641
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003642@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003643def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003644 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003645 parser.add_option('-d', '--display', action='store_true',
3646 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003647 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003648 help='New description to set for this issue (- for stdin, '
3649 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003650 parser.add_option('-f', '--force', action='store_true',
3651 help='Delete any unpublished Gerrit edits for this issue '
3652 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003653
3654 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003655 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003656 options, args = parser.parse_args(args)
3657 _process_codereview_select_options(parser, options)
3658
3659 target_issue = None
3660 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003661 target_issue = ParseIssueNumberArgument(args[0])
3662 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003663 parser.print_help()
3664 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003665
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003666 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003667
martiniss6eda05f2016-06-30 10:18:35 -07003668 kwargs = {
3669 'auth_config': auth_config,
3670 'codereview': options.forced_codereview,
3671 }
3672 if target_issue:
3673 kwargs['issue'] = target_issue.issue
3674 if options.forced_codereview == 'rietveld':
3675 kwargs['rietveld_server'] = target_issue.hostname
3676
3677 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003678
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003679 if not cl.GetIssue():
3680 DieWithError('This branch has no associated changelist.')
3681 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003682
smut@google.com34fb6b12015-07-13 20:03:26 +00003683 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003684 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003685 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003686
3687 if options.new_description:
3688 text = options.new_description
3689 if text == '-':
3690 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003691 elif text == '+':
3692 base_branch = cl.GetCommonAncestorWithUpstream()
3693 change = cl.GetChange(base_branch, None, local_description=True)
3694 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003695
3696 description.set_description(text)
3697 else:
3698 description.prompt()
3699
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003700 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003701 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003702 return 0
3703
3704
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003705def CreateDescriptionFromLog(args):
3706 """Pulls out the commit log to use as a base for the CL description."""
3707 log_args = []
3708 if len(args) == 1 and not args[0].endswith('.'):
3709 log_args = [args[0] + '..']
3710 elif len(args) == 1 and args[0].endswith('...'):
3711 log_args = [args[0][:-1]]
3712 elif len(args) == 2:
3713 log_args = [args[0] + '..' + args[1]]
3714 else:
3715 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003716 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717
3718
thestig@chromium.org44202a22014-03-11 19:22:18 +00003719def CMDlint(parser, args):
3720 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003721 parser.add_option('--filter', action='append', metavar='-x,+y',
3722 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003723 auth.add_auth_options(parser)
3724 options, args = parser.parse_args(args)
3725 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003726
3727 # Access to a protected member _XX of a client class
3728 # pylint: disable=W0212
3729 try:
3730 import cpplint
3731 import cpplint_chromium
3732 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003733 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003734 return 1
3735
3736 # Change the current working directory before calling lint so that it
3737 # shows the correct base.
3738 previous_cwd = os.getcwd()
3739 os.chdir(settings.GetRoot())
3740 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003741 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003742 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3743 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003744 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003745 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003746 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003747
3748 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003749 command = args + files
3750 if options.filter:
3751 command = ['--filter=' + ','.join(options.filter)] + command
3752 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003753
3754 white_regex = re.compile(settings.GetLintRegex())
3755 black_regex = re.compile(settings.GetLintIgnoreRegex())
3756 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3757 for filename in filenames:
3758 if white_regex.match(filename):
3759 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003760 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003761 else:
3762 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3763 extra_check_functions)
3764 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003765 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003766 finally:
3767 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003768 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003769 if cpplint._cpplint_state.error_count != 0:
3770 return 1
3771 return 0
3772
3773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003774def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003775 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003776 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003777 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003778 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003779 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003780 auth.add_auth_options(parser)
3781 options, args = parser.parse_args(args)
3782 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003783
sbc@chromium.org71437c02015-04-09 19:29:40 +00003784 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786 return 1
3787
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003788 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003789 if args:
3790 base_branch = args[0]
3791 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003792 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003793 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003794
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003795 cl.RunHook(
3796 committing=not options.upload,
3797 may_prompt=False,
3798 verbose=options.verbose,
3799 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003800 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003801
3802
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003803def GenerateGerritChangeId(message):
3804 """Returns Ixxxxxx...xxx change id.
3805
3806 Works the same way as
3807 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3808 but can be called on demand on all platforms.
3809
3810 The basic idea is to generate git hash of a state of the tree, original commit
3811 message, author/committer info and timestamps.
3812 """
3813 lines = []
3814 tree_hash = RunGitSilent(['write-tree'])
3815 lines.append('tree %s' % tree_hash.strip())
3816 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3817 if code == 0:
3818 lines.append('parent %s' % parent.strip())
3819 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3820 lines.append('author %s' % author.strip())
3821 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3822 lines.append('committer %s' % committer.strip())
3823 lines.append('')
3824 # Note: Gerrit's commit-hook actually cleans message of some lines and
3825 # whitespace. This code is not doing this, but it clearly won't decrease
3826 # entropy.
3827 lines.append(message)
3828 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3829 stdin='\n'.join(lines))
3830 return 'I%s' % change_hash.strip()
3831
3832
wittman@chromium.org455dc922015-01-26 20:15:50 +00003833def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3834 """Computes the remote branch ref to use for the CL.
3835
3836 Args:
3837 remote (str): The git remote for the CL.
3838 remote_branch (str): The git remote branch for the CL.
3839 target_branch (str): The target branch specified by the user.
3840 pending_prefix (str): The pending prefix from the settings.
3841 """
3842 if not (remote and remote_branch):
3843 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003844
wittman@chromium.org455dc922015-01-26 20:15:50 +00003845 if target_branch:
3846 # Cannonicalize branch references to the equivalent local full symbolic
3847 # refs, which are then translated into the remote full symbolic refs
3848 # below.
3849 if '/' not in target_branch:
3850 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3851 else:
3852 prefix_replacements = (
3853 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3854 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3855 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3856 )
3857 match = None
3858 for regex, replacement in prefix_replacements:
3859 match = re.search(regex, target_branch)
3860 if match:
3861 remote_branch = target_branch.replace(match.group(0), replacement)
3862 break
3863 if not match:
3864 # This is a branch path but not one we recognize; use as-is.
3865 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003866 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3867 # Handle the refs that need to land in different refs.
3868 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003869
wittman@chromium.org455dc922015-01-26 20:15:50 +00003870 # Create the true path to the remote branch.
3871 # Does the following translation:
3872 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3873 # * refs/remotes/origin/master -> refs/heads/master
3874 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3875 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3876 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3877 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3878 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3879 'refs/heads/')
3880 elif remote_branch.startswith('refs/remotes/branch-heads'):
3881 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3882 # If a pending prefix exists then replace refs/ with it.
3883 if pending_prefix:
3884 remote_branch = remote_branch.replace('refs/', pending_prefix)
3885 return remote_branch
3886
3887
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003888def cleanup_list(l):
3889 """Fixes a list so that comma separated items are put as individual items.
3890
3891 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3892 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3893 """
3894 items = sum((i.split(',') for i in l), [])
3895 stripped_items = (i.strip() for i in items)
3896 return sorted(filter(None, stripped_items))
3897
3898
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003899@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003900def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003901 """Uploads the current changelist to codereview.
3902
3903 Can skip dependency patchset uploads for a branch by running:
3904 git config branch.branch_name.skip-deps-uploads True
3905 To unset run:
3906 git config --unset branch.branch_name.skip-deps-uploads
3907 Can also set the above globally by using the --global flag.
3908 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003909 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3910 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003911 parser.add_option('--bypass-watchlists', action='store_true',
3912 dest='bypass_watchlists',
3913 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003914 parser.add_option('-f', action='store_true', dest='force',
3915 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003916 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003917 parser.add_option('-b', '--bug',
3918 help='pre-populate the bug number(s) for this issue. '
3919 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003920 parser.add_option('--message-file', dest='message_file',
3921 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003922 parser.add_option('-t', dest='title',
3923 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003924 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003925 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003926 help='reviewer email addresses')
3927 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003928 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003929 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003930 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003931 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003932 parser.add_option('--emulate_svn_auto_props',
3933 '--emulate-svn-auto-props',
3934 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003935 dest="emulate_svn_auto_props",
3936 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003937 parser.add_option('-c', '--use-commit-queue', action='store_true',
3938 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003939 parser.add_option('--private', action='store_true',
3940 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003941 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003942 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003943 metavar='TARGET',
3944 help='Apply CL to remote ref TARGET. ' +
3945 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003946 parser.add_option('--squash', action='store_true',
3947 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003948 parser.add_option('--no-squash', action='store_true',
3949 help='Don\'t squash multiple commits into one ' +
3950 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07003951 parser.add_option('--topic', default=None,
3952 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003953 parser.add_option('--email', default=None,
3954 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003955 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3956 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003957 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3958 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003959 help='Send the patchset to do a CQ dry run right after '
3960 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003961 parser.add_option('--dependencies', action='store_true',
3962 help='Uploads CLs of all the local branches that depend on '
3963 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003964
rmistry@google.com2dd99862015-06-22 12:22:18 +00003965 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003966 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003967 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003968 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003969 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003970 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003971 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003972
sbc@chromium.org71437c02015-04-09 19:29:40 +00003973 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003974 return 1
3975
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003976 options.reviewers = cleanup_list(options.reviewers)
3977 options.cc = cleanup_list(options.cc)
3978
tandriib80458a2016-06-23 12:20:07 -07003979 if options.message_file:
3980 if options.message:
3981 parser.error('only one of --message and --message-file allowed.')
3982 options.message = gclient_utils.FileRead(options.message_file)
3983 options.message_file = None
3984
tandrii4d0545a2016-07-06 03:56:49 -07003985 if options.cq_dry_run and options.use_commit_queue:
3986 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3987
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003988 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3989 settings.GetIsGerrit()
3990
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003991 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003992 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003993
3994
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003995def IsSubmoduleMergeCommit(ref):
3996 # When submodules are added to the repo, we expect there to be a single
3997 # non-git-svn merge commit at remote HEAD with a signature comment.
3998 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003999 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004000 return RunGit(cmd) != ''
4001
4002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004004 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004006 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4007 upstream and closes the issue automatically and atomically.
4008
4009 Otherwise (in case of Rietveld):
4010 Squashes branch into a single commit.
4011 Updates changelog with metadata (e.g. pointer to review).
4012 Pushes/dcommits the code upstream.
4013 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004014 """
4015 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4016 help='bypass upload presubmit hook')
4017 parser.add_option('-m', dest='message',
4018 help="override review description")
4019 parser.add_option('-f', action='store_true', dest='force',
4020 help="force yes to questions (don't prompt)")
4021 parser.add_option('-c', dest='contributor',
4022 help="external contributor for patch (appended to " +
4023 "description and used as author for git). Should be " +
4024 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004025 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004026 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004027 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004028 auth_config = auth.extract_auth_config_from_options(options)
4029
4030 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004032 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4033 if cl.IsGerrit():
4034 if options.message:
4035 # This could be implemented, but it requires sending a new patch to
4036 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4037 # Besides, Gerrit has the ability to change the commit message on submit
4038 # automatically, thus there is no need to support this option (so far?).
4039 parser.error('-m MESSAGE option is not supported for Gerrit.')
4040 if options.contributor:
4041 parser.error(
4042 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4043 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4044 'the contributor\'s "name <email>". If you can\'t upload such a '
4045 'commit for review, contact your repository admin and request'
4046 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004047 if not cl.GetIssue():
4048 DieWithError('You must upload the issue first to Gerrit.\n'
4049 ' If you would rather have `git cl land` upload '
4050 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004051 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4052 options.verbose)
4053
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004054 current = cl.GetBranch()
4055 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4056 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004057 print()
4058 print('Attempting to push branch %r into another local branch!' % current)
4059 print()
4060 print('Either reparent this branch on top of origin/master:')
4061 print(' git reparent-branch --root')
4062 print()
4063 print('OR run `git rebase-update` if you think the parent branch is ')
4064 print('already committed.')
4065 print()
4066 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004067 return 1
4068
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004069 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070 # Default to merging against our best guess of the upstream branch.
4071 args = [cl.GetUpstreamBranch()]
4072
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004073 if options.contributor:
4074 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004075 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004076 return 1
4077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004079 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080
sbc@chromium.org71437c02015-04-09 19:29:40 +00004081 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082 return 1
4083
4084 # This rev-list syntax means "show all commits not in my branch that
4085 # are in base_branch".
4086 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4087 base_branch]).splitlines()
4088 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print('Base branch "%s" has %d commits '
4090 'not in this branch.' % (base_branch, len(upstream_commits)))
4091 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092 return 1
4093
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004094 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004095 svn_head = None
4096 if cmd == 'dcommit' or base_has_submodules:
4097 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4098 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004099
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004101 # If the base_head is a submodule merge commit, the first parent of the
4102 # base_head should be a git-svn commit, which is what we're interested in.
4103 base_svn_head = base_branch
4104 if base_has_submodules:
4105 base_svn_head += '^1'
4106
4107 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004109 print('This branch has %d additional commits not upstreamed yet.'
4110 % len(extra_commits.splitlines()))
4111 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4112 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004113 return 1
4114
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004115 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004116 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004117 author = None
4118 if options.contributor:
4119 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004120 hook_results = cl.RunHook(
4121 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004122 may_prompt=not options.force,
4123 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004124 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004125 if not hook_results.should_continue():
4126 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004127
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004128 # Check the tree status if the tree status URL is set.
4129 status = GetTreeStatus()
4130 if 'closed' == status:
4131 print('The tree is closed. Please wait for it to reopen. Use '
4132 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4133 return 1
4134 elif 'unknown' == status:
4135 print('Unable to determine tree status. Please verify manually and '
4136 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4137 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004139 change_desc = ChangeDescription(options.message)
4140 if not change_desc.description and cl.GetIssue():
4141 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004143 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004144 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004145 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004146 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004147 print('No description set.')
4148 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004149 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004150
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004151 # Keep a separate copy for the commit message, because the commit message
4152 # contains the link to the Rietveld issue, while the Rietveld message contains
4153 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004154 # Keep a separate copy for the commit message.
4155 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004156 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004157
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004158 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004159 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004160 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004161 # after it. Add a period on a new line to circumvent this. Also add a space
4162 # before the period to make sure that Gitiles continues to correctly resolve
4163 # the URL.
4164 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004165 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004166 commit_desc.append_footer('Patch from %s.' % options.contributor)
4167
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004168 print('Description:')
4169 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004170
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004171 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004172 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004173 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004174
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004175 # We want to squash all this branch's commits into one commit with the proper
4176 # description. We do this by doing a "reset --soft" to the base branch (which
4177 # keeps the working copy the same), then dcommitting that. If origin/master
4178 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4179 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004181 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4182 # Delete the branches if they exist.
4183 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4184 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4185 result = RunGitWithCode(showref_cmd)
4186 if result[0] == 0:
4187 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004188
4189 # We might be in a directory that's present in this branch but not in the
4190 # trunk. Move up to the top of the tree so that git commands that expect a
4191 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004192 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193 if rel_base_path:
4194 os.chdir(rel_base_path)
4195
4196 # Stuff our change into the merge branch.
4197 # We wrap in a try...finally block so if anything goes wrong,
4198 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004199 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004200 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004201 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004202 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004204 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004205 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004206 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004207 RunGit(
4208 [
4209 'commit', '--author', options.contributor,
4210 '-m', commit_desc.description,
4211 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004213 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004214 if base_has_submodules:
4215 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4216 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4217 RunGit(['checkout', CHERRY_PICK_BRANCH])
4218 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004219 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004220 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004221 mirror = settings.GetGitMirror(remote)
4222 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004223 pending_prefix = settings.GetPendingRefPrefix()
4224 if not pending_prefix or branch.startswith(pending_prefix):
4225 # If not using refs/pending/heads/* at all, or target ref is already set
4226 # to pending, then push to the target ref directly.
4227 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004228 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004229 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004230 else:
4231 # Cherry-pick the change on top of pending ref and then push it.
4232 assert branch.startswith('refs/'), branch
4233 assert pending_prefix[-1] == '/', pending_prefix
4234 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004235 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004236 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004237 if retcode == 0:
4238 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004239 else:
4240 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004241 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004242 'svn', 'dcommit',
4243 '-C%s' % options.similarity,
4244 '--no-rebase', '--rmdir',
4245 ]
4246 if settings.GetForceHttpsCommitUrl():
4247 # Allow forcing https commit URLs for some projects that don't allow
4248 # committing to http URLs (like Google Code).
4249 remote_url = cl.GetGitSvnRemoteUrl()
4250 if urlparse.urlparse(remote_url).scheme == 'http':
4251 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004252 cmd_args.append('--commit-url=%s' % remote_url)
4253 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004254 if 'Committed r' in output:
4255 revision = re.match(
4256 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4257 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 finally:
4259 # And then swap back to the original branch and clean up.
4260 RunGit(['checkout', '-q', cl.GetBranch()])
4261 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004262 if base_has_submodules:
4263 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004265 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004266 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004267 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004268
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004269 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004270 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004271 try:
4272 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4273 # We set pushed_to_pending to False, since it made it all the way to the
4274 # real ref.
4275 pushed_to_pending = False
4276 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004277 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004279 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004280 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004282 if not to_pending:
4283 if viewvc_url and revision:
4284 change_desc.append_footer(
4285 'Committed: %s%s' % (viewvc_url, revision))
4286 elif revision:
4287 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004288 print('Closing issue '
4289 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004290 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004291 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004292 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004293 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004294 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004295 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004296 if options.bypass_hooks:
4297 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4298 else:
4299 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004300 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004301
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004302 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004303 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('The commit is in the pending queue (%s).' % pending_ref)
4305 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4306 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004307
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004308 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4309 if os.path.isfile(hook):
4310 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004311
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004312 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313
4314
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004315def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004316 print()
4317 print('Waiting for commit to be landed on %s...' % real_ref)
4318 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004319 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4320 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004321 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004322
4323 loop = 0
4324 while True:
4325 sys.stdout.write('fetching (%d)... \r' % loop)
4326 sys.stdout.flush()
4327 loop += 1
4328
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004329 if mirror:
4330 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004331 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4332 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4333 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4334 for commit in commits.splitlines():
4335 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004336 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004337 return commit
4338
4339 current_rev = to_rev
4340
4341
tandriibf429402016-09-14 07:09:12 -07004342def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004343 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4344
4345 Returns:
4346 (retcode of last operation, output log of last operation).
4347 """
4348 assert pending_ref.startswith('refs/'), pending_ref
4349 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4350 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4351 code = 0
4352 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004353 max_attempts = 3
4354 attempts_left = max_attempts
4355 while attempts_left:
4356 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004358 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004359
4360 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004361 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004362 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004363 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004364 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004365 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004366 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004367 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004368 continue
4369
4370 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004371 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004372 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004373 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004374 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004375 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4376 'the following files have merge conflicts:' % pending_ref)
4377 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4378 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004379 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004380 return code, out
4381
4382 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004384 code, out = RunGitWithCode(
4385 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4386 if code == 0:
4387 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004388 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004389 return code, out
4390
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004392 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004394 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004395 print('Fatal push error. Make sure your .netrc credentials and git '
4396 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004397 return code, out
4398
vapiera7fbd5a2016-06-16 09:17:49 -07004399 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004400 return code, out
4401
4402
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004403def IsFatalPushFailure(push_stdout):
4404 """True if retrying push won't help."""
4405 return '(prohibited by Gerrit)' in push_stdout
4406
4407
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004408@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004409def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004410 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004412 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004413 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004414 message = """This repository appears to be a git-svn mirror, but we
4415don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004416 else:
4417 message = """This doesn't appear to be an SVN repository.
4418If your project has a true, writeable git repository, you probably want to run
4419'git cl land' instead.
4420If your project has a git mirror of an upstream SVN master, you probably need
4421to run 'git svn init'.
4422
4423Using the wrong command might cause your commit to appear to succeed, and the
4424review to be closed, without actually landing upstream. If you choose to
4425proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004426 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004427 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004428 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4429 'Please let us know of this project you are committing to:'
4430 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431 return SendUpstream(parser, args, 'dcommit')
4432
4433
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004434@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004435def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004436 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004437 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004438 print('This appears to be an SVN repository.')
4439 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004440 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004441 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004442 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443
4444
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004445@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004447 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004448 parser.add_option('-b', dest='newbranch',
4449 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004450 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004452 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4453 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004454 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004455 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004456 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004457 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004458 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004459 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004460
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004461
4462 group = optparse.OptionGroup(
4463 parser,
4464 'Options for continuing work on the current issue uploaded from a '
4465 'different clone (e.g. different machine). Must be used independently '
4466 'from the other options. No issue number should be specified, and the '
4467 'branch must have an issue number associated with it')
4468 group.add_option('--reapply', action='store_true', dest='reapply',
4469 help='Reset the branch and reapply the issue.\n'
4470 'CAUTION: This will undo any local changes in this '
4471 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004472
4473 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004474 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004475 parser.add_option_group(group)
4476
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004477 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004478 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004480 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004481 auth_config = auth.extract_auth_config_from_options(options)
4482
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004483
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004484 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004485 if options.newbranch:
4486 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004487 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004488 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004489
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004490 cl = Changelist(auth_config=auth_config,
4491 codereview=options.forced_codereview)
4492 if not cl.GetIssue():
4493 parser.error('current branch must have an associated issue')
4494
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004495 upstream = cl.GetUpstreamBranch()
4496 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004497 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004498
4499 RunGit(['reset', '--hard', upstream])
4500 if options.pull:
4501 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004502
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004503 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4504 options.directory)
4505
4506 if len(args) != 1 or not args[0]:
4507 parser.error('Must specify issue number or url')
4508
4509 # We don't want uncommitted changes mixed up with the patch.
4510 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004511 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004512
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004513 if options.newbranch:
4514 if options.force:
4515 RunGit(['branch', '-D', options.newbranch],
4516 stderr=subprocess2.PIPE, error_ok=True)
4517 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004518 elif not GetCurrentBranch():
4519 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004520
4521 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4522
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004523 if cl.IsGerrit():
4524 if options.reject:
4525 parser.error('--reject is not supported with Gerrit codereview.')
4526 if options.nocommit:
4527 parser.error('--nocommit is not supported with Gerrit codereview.')
4528 if options.directory:
4529 parser.error('--directory is not supported with Gerrit codereview.')
4530
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004531 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004532 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533
4534
4535def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004536 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 # Provide a wrapper for git svn rebase to help avoid accidental
4538 # git svn dcommit.
4539 # It's the only command that doesn't use parser at all since we just defer
4540 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004541
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004542 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543
4544
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004545def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004546 """Fetches the tree status and returns either 'open', 'closed',
4547 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004548 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004549 if url:
4550 status = urllib2.urlopen(url).read().lower()
4551 if status.find('closed') != -1 or status == '0':
4552 return 'closed'
4553 elif status.find('open') != -1 or status == '1':
4554 return 'open'
4555 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556 return 'unset'
4557
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004559def GetTreeStatusReason():
4560 """Fetches the tree status from a json url and returns the message
4561 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004562 url = settings.GetTreeStatusUrl()
4563 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004564 connection = urllib2.urlopen(json_url)
4565 status = json.loads(connection.read())
4566 connection.close()
4567 return status['message']
4568
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004569
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004570def GetBuilderMaster(bot_list):
4571 """For a given builder, fetch the master from AE if available."""
4572 map_url = 'https://builders-map.appspot.com/'
4573 try:
4574 master_map = json.load(urllib2.urlopen(map_url))
4575 except urllib2.URLError as e:
4576 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4577 (map_url, e))
4578 except ValueError as e:
4579 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4580 if not master_map:
4581 return None, 'Failed to build master map.'
4582
4583 result_master = ''
4584 for bot in bot_list:
4585 builder = bot.split(':', 1)[0]
4586 master_list = master_map.get(builder, [])
4587 if not master_list:
4588 return None, ('No matching master for builder %s.' % builder)
4589 elif len(master_list) > 1:
4590 return None, ('The builder name %s exists in multiple masters %s.' %
4591 (builder, master_list))
4592 else:
4593 cur_master = master_list[0]
4594 if not result_master:
4595 result_master = cur_master
4596 elif result_master != cur_master:
4597 return None, 'The builders do not belong to the same master.'
4598 return result_master, None
4599
4600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004601def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004602 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004603 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604 status = GetTreeStatus()
4605 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004606 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004607 return 2
4608
vapiera7fbd5a2016-06-16 09:17:49 -07004609 print('The tree is %s' % status)
4610 print()
4611 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612 if status != 'open':
4613 return 1
4614 return 0
4615
4616
maruel@chromium.org15192402012-09-06 12:38:29 +00004617def CMDtry(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004618 '''Triggers try jobs through BuildBucket.'''
4619 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004620 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004621 '-b', '--bot', action='append',
4622 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4623 'times to specify multiple builders. ex: '
4624 '"-b win_rel -b win_layout". See '
4625 'the try server waterfall for the builders name and the tests '
4626 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004627 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004628 '-m', '--master', default='',
4629 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004630 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004631 '-r', '--revision',
4632 help='Revision to use for the try job; default: the '
4633 'revision will be determined by the try server; see '
4634 'its waterfall for more info')
maruel@chromium.org15192402012-09-06 12:38:29 +00004635 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004636 '-c', '--clobber', action='store_true', default=False,
4637 help='Force a clobber before building; e.g. don\'t do an '
4638 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004639 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004640 '--project',
4641 help='Override which project to use. Projects are defined '
4642 'server-side to define what default bot set to use')
maruel@chromium.org15192402012-09-06 12:38:29 +00004643 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004644 '-p', '--property', dest='properties', action='append', default=[],
4645 help='Specify generic properties in the form -p key1=value1 -p '
4646 'key2=value2 etc (buildbucket only). The value will be treated as '
4647 'json if decodable, or as string otherwise.')
machenbach@chromium.org45453142015-09-15 08:45:22 +00004648 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004649 '-n', '--name', help='Try job name; default to current branch name')
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004650 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004651 '--use-rietveld', action='store_true', default=False,
4652 help='Use Rietveld to trigger try jobs.')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004653 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004654 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4655 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004656 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004657 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004658 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004659 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004660
machenbach@chromium.org45453142015-09-15 08:45:22 +00004661 if options.use_rietveld and options.properties:
4662 parser.error('Properties can only be specified with buildbucket')
4663
4664 # Make sure that all properties are prop=value pairs.
4665 bad_params = [x for x in options.properties if '=' not in x]
4666 if bad_params:
4667 parser.error('Got properties with missing "=": %s' % bad_params)
4668
maruel@chromium.org15192402012-09-06 12:38:29 +00004669 if args:
4670 parser.error('Unknown arguments: %s' % args)
4671
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004672 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004673 if not cl.GetIssue():
4674 parser.error('Need to upload first')
4675
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004676 if cl.IsGerrit():
4677 parser.error(
4678 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4679 'If your project has Commit Queue, dry run is a workaround:\n'
4680 ' git cl set-commit --dry-run')
4681 # Code below assumes Rietveld issue.
4682 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4683
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004684 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004685 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004686 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004687
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004688 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004689 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004690
maruel@chromium.org15192402012-09-06 12:38:29 +00004691 if not options.name:
4692 options.name = cl.GetBranch()
4693
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004694 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004695 options.master, err_msg = GetBuilderMaster(options.bot)
4696 if err_msg:
4697 parser.error('Tryserver master cannot be found because: %s\n'
4698 'Please manually specify the tryserver master'
4699 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004700
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004701 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004702 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004703 if not options.bot:
4704 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004705
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004706 # Get try masters from PRESUBMIT.py files.
4707 masters = presubmit_support.DoGetTryMasters(
4708 change,
4709 change.LocalPaths(),
4710 settings.GetRoot(),
4711 None,
4712 None,
4713 options.verbose,
4714 sys.stdout)
4715 if masters:
4716 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004717
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004718 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4719 options.bot = presubmit_support.DoGetTrySlaves(
4720 change,
4721 change.LocalPaths(),
4722 settings.GetRoot(),
4723 None,
4724 None,
4725 options.verbose,
4726 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004727
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004728 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004729 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004730
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004731 builders_and_tests = {}
4732 # TODO(machenbach): The old style command-line options don't support
4733 # multiple try masters yet.
4734 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4735 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4736
4737 for bot in old_style:
4738 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004739 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004740 elif ',' in bot:
4741 parser.error('Specify one bot per --bot flag')
4742 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004743 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004744
4745 for bot, tests in new_style:
4746 builders_and_tests.setdefault(bot, []).extend(tests)
4747
4748 # Return a master map with one master to be backwards compatible. The
4749 # master name defaults to an empty string, which will cause the master
4750 # not to be set on rietveld (deprecated).
4751 return {options.master: builders_and_tests}
4752
4753 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004754 if not masters:
4755 # Default to triggering Dry Run (see http://crbug.com/625697).
4756 if options.verbose:
4757 print('git cl try with no bots now defaults to CQ Dry Run.')
4758 try:
4759 cl.SetCQState(_CQState.DRY_RUN)
4760 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4761 return 0
4762 except KeyboardInterrupt:
4763 raise
4764 except:
4765 print('WARNING: failed to trigger CQ Dry Run.\n'
4766 'Either:\n'
4767 ' * your project has no CQ\n'
4768 ' * you don\'t have permission to trigger Dry Run\n'
4769 ' * bug in this code (see stack trace below).\n'
4770 'Consider specifying which bots to trigger manually '
4771 'or asking your project owners for permissions '
4772 'or contacting Chrome Infrastructure team at '
4773 'https://www.chromium.org/infra\n\n')
4774 # Still raise exception so that stack trace is printed.
4775 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004776
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004777 for builders in masters.itervalues():
4778 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004779 print('ERROR You are trying to send a job to a triggered bot. This type '
4780 'of bot requires an\ninitial job from a parent (usually a builder).'
4781 ' Instead send your job to the parent.\n'
4782 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004783 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004784
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004785 patchset = cl.GetMostRecentPatchset()
4786 if patchset and patchset != cl.GetPatchset():
4787 print(
4788 '\nWARNING Mismatch between local config and server. Did a previous '
4789 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4790 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004791 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004792 try:
4793 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4794 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004795 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004796 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004797 except Exception as e:
4798 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004799 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004800 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004801 return 1
4802 else:
4803 try:
4804 cl.RpcServer().trigger_distributed_try_jobs(
4805 cl.GetIssue(), patchset, options.name, options.clobber,
4806 options.revision, masters)
4807 except urllib2.HTTPError as e:
4808 if e.code == 404:
4809 print('404 from rietveld; '
4810 'did you mean to use "git try" instead of "git cl try"?')
4811 return 1
4812 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004813
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004814 for (master, builders) in sorted(masters.iteritems()):
4815 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004816 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004817 length = max(len(builder) for builder in builders)
4818 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004819 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004820 return 0
4821
4822
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004823def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004824 """Prints info about try jobs associated with current CL."""
4825 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004828 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004829 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004830 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004831 '--color', action='store_true', default=setup_color.IS_TTY,
4832 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004833 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004834 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4835 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004836 group.add_option(
4837 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004838 parser.add_option_group(group)
4839 auth.add_auth_options(parser)
4840 options, args = parser.parse_args(args)
4841 if args:
4842 parser.error('Unrecognized args: %s' % ' '.join(args))
4843
4844 auth_config = auth.extract_auth_config_from_options(options)
4845 cl = Changelist(auth_config=auth_config)
4846 if not cl.GetIssue():
4847 parser.error('Need to upload first')
4848
tandrii221ab252016-10-06 08:12:04 -07004849 patchset = options.patchset
4850 if not patchset:
4851 patchset = cl.GetMostRecentPatchset()
4852 if not patchset:
4853 parser.error('Codereview doesn\'t know about issue %s. '
4854 'No access to issue or wrong issue number?\n'
4855 'Either upload first, or pass --patchset explicitely' %
4856 cl.GetIssue())
4857
4858 if patchset != cl.GetPatchset():
4859 print('WARNING: Mismatch between local config and server. Did a previous '
4860 'upload fail?\n'
4861 'By default, git cl try uses latest patchset from codereview.\n'
4862 'Continuing using patchset %s.\n' % patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004863 try:
tandrii221ab252016-10-06 08:12:04 -07004864 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004865 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004866 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004867 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004868 if options.json:
4869 write_try_results_json(options.json, jobs)
4870 else:
4871 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004872 return 0
4873
4874
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004875@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004876def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004877 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004878 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004879 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004880 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004882 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004883 if args:
4884 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004885 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004886 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004887 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004888 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004889
4890 # Clear configured merge-base, if there is one.
4891 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004892 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004893 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004894 return 0
4895
4896
thestig@chromium.org00858c82013-12-02 23:08:03 +00004897def CMDweb(parser, args):
4898 """Opens the current CL in the web browser."""
4899 _, args = parser.parse_args(args)
4900 if args:
4901 parser.error('Unrecognized args: %s' % ' '.join(args))
4902
4903 issue_url = Changelist().GetIssueURL()
4904 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004905 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004906 return 1
4907
4908 webbrowser.open(issue_url)
4909 return 0
4910
4911
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004912def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004913 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004914 parser.add_option('-d', '--dry-run', action='store_true',
4915 help='trigger in dry run mode')
4916 parser.add_option('-c', '--clear', action='store_true',
4917 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004918 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004919 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004920 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004921 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004922 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004923 if args:
4924 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004925 if options.dry_run and options.clear:
4926 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4927
iannuccie53c9352016-08-17 14:40:40 -07004928 cl = Changelist(auth_config=auth_config, issue=options.issue,
4929 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004930 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004931 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004932 elif options.dry_run:
4933 state = _CQState.DRY_RUN
4934 else:
4935 state = _CQState.COMMIT
4936 if not cl.GetIssue():
4937 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004938 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004939 return 0
4940
4941
groby@chromium.org411034a2013-02-26 15:12:01 +00004942def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004943 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004944 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004945 auth.add_auth_options(parser)
4946 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004947 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004948 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004949 if args:
4950 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004951 cl = Changelist(auth_config=auth_config, issue=options.issue,
4952 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004953 # Ensure there actually is an issue to close.
4954 cl.GetDescription()
4955 cl.CloseIssue()
4956 return 0
4957
4958
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004959def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004960 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004961 parser.add_option(
4962 '--stat',
4963 action='store_true',
4964 dest='stat',
4965 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004966 auth.add_auth_options(parser)
4967 options, args = parser.parse_args(args)
4968 auth_config = auth.extract_auth_config_from_options(options)
4969 if args:
4970 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004971
4972 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004973 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004974 # Staged changes would be committed along with the patch from last
4975 # upload, hence counted toward the "last upload" side in the final
4976 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004977 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004978 return 1
4979
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004980 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004981 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004982 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004983 if not issue:
4984 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004985 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004986 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004987
4988 # Create a new branch based on the merge-base
4989 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004990 # Clear cached branch in cl object, to avoid overwriting original CL branch
4991 # properties.
4992 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004993 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004994 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004995 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004996 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004997 return rtn
4998
wychen@chromium.org06928532015-02-03 02:11:29 +00004999 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005000 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005001 cmd = ['git', 'diff']
5002 if options.stat:
5003 cmd.append('--stat')
5004 cmd.extend([TMP_BRANCH, branch, '--'])
5005 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005006 finally:
5007 RunGit(['checkout', '-q', branch])
5008 RunGit(['branch', '-D', TMP_BRANCH])
5009
5010 return 0
5011
5012
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005013def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005014 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005015 parser.add_option(
5016 '--no-color',
5017 action='store_true',
5018 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005019 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005020 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005021 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005022
5023 author = RunGit(['config', 'user.email']).strip() or None
5024
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005025 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005026
5027 if args:
5028 if len(args) > 1:
5029 parser.error('Unknown args')
5030 base_branch = args[0]
5031 else:
5032 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005033 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005034
5035 change = cl.GetChange(base_branch, None)
5036 return owners_finder.OwnersFinder(
5037 [f.LocalPath() for f in
5038 cl.GetChange(base_branch, None).AffectedFiles()],
5039 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005040 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005041 disable_color=options.no_color).run()
5042
5043
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005044def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005045 """Generates a diff command."""
5046 # Generate diff for the current branch's changes.
5047 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5048 upstream_commit, '--' ]
5049
5050 if args:
5051 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005052 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005053 diff_cmd.append(arg)
5054 else:
5055 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005056
5057 return diff_cmd
5058
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005059def MatchingFileType(file_name, extensions):
5060 """Returns true if the file name ends with one of the given extensions."""
5061 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005062
enne@chromium.org555cfe42014-01-29 18:21:39 +00005063@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005064def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005065 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005066 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005067 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005068 parser.add_option('--full', action='store_true',
5069 help='Reformat the full content of all touched files')
5070 parser.add_option('--dry-run', action='store_true',
5071 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005072 parser.add_option('--python', action='store_true',
5073 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005074 parser.add_option('--diff', action='store_true',
5075 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005076 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005077
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005078 # git diff generates paths against the root of the repository. Change
5079 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005080 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005081 if rel_base_path:
5082 os.chdir(rel_base_path)
5083
digit@chromium.org29e47272013-05-17 17:01:46 +00005084 # Grab the merge-base commit, i.e. the upstream commit of the current
5085 # branch when it was created or the last time it was rebased. This is
5086 # to cover the case where the user may have called "git fetch origin",
5087 # moving the origin branch to a newer commit, but hasn't rebased yet.
5088 upstream_commit = None
5089 cl = Changelist()
5090 upstream_branch = cl.GetUpstreamBranch()
5091 if upstream_branch:
5092 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5093 upstream_commit = upstream_commit.strip()
5094
5095 if not upstream_commit:
5096 DieWithError('Could not find base commit for this branch. '
5097 'Are you in detached state?')
5098
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005099 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5100 diff_output = RunGit(changed_files_cmd)
5101 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005102 # Filter out files deleted by this CL
5103 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005104
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005105 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5106 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5107 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005108 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005109
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005110 top_dir = os.path.normpath(
5111 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5112
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005113 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5114 # formatted. This is used to block during the presubmit.
5115 return_value = 0
5116
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005117 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005118 # Locate the clang-format binary in the checkout
5119 try:
5120 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005121 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005122 DieWithError(e)
5123
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005124 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005125 cmd = [clang_format_tool]
5126 if not opts.dry_run and not opts.diff:
5127 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005129 if opts.diff:
5130 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005131 else:
5132 env = os.environ.copy()
5133 env['PATH'] = str(os.path.dirname(clang_format_tool))
5134 try:
5135 script = clang_format.FindClangFormatScriptInChromiumTree(
5136 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005137 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005138 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005139
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005140 cmd = [sys.executable, script, '-p0']
5141 if not opts.dry_run and not opts.diff:
5142 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005143
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005144 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5145 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005146
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005147 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5148 if opts.diff:
5149 sys.stdout.write(stdout)
5150 if opts.dry_run and len(stdout) > 0:
5151 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005152
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005153 # Similar code to above, but using yapf on .py files rather than clang-format
5154 # on C/C++ files
5155 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005156 yapf_tool = gclient_utils.FindExecutable('yapf')
5157 if yapf_tool is None:
5158 DieWithError('yapf not found in PATH')
5159
5160 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005161 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005162 cmd = [yapf_tool]
5163 if not opts.dry_run and not opts.diff:
5164 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005165 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005166 if opts.diff:
5167 sys.stdout.write(stdout)
5168 else:
5169 # TODO(sbc): yapf --lines mode still has some issues.
5170 # https://github.com/google/yapf/issues/154
5171 DieWithError('--python currently only works with --full')
5172
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005173 # Dart's formatter does not have the nice property of only operating on
5174 # modified chunks, so hard code full.
5175 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005176 try:
5177 command = [dart_format.FindDartFmtToolInChromiumTree()]
5178 if not opts.dry_run and not opts.diff:
5179 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005180 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005181
ppi@chromium.org6593d932016-03-03 15:41:15 +00005182 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005183 if opts.dry_run and stdout:
5184 return_value = 2
5185 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005186 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5187 'found in this checkout. Files in other languages are still '
5188 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005189
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005190 # Format GN build files. Always run on full build files for canonical form.
5191 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005192 cmd = ['gn', 'format' ]
5193 if opts.dry_run or opts.diff:
5194 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005195 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005196 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5197 shell=sys.platform == 'win32',
5198 cwd=top_dir)
5199 if opts.dry_run and gn_ret == 2:
5200 return_value = 2 # Not formatted.
5201 elif opts.diff and gn_ret == 2:
5202 # TODO this should compute and print the actual diff.
5203 print("This change has GN build file diff for " + gn_diff_file)
5204 elif gn_ret != 0:
5205 # For non-dry run cases (and non-2 return values for dry-run), a
5206 # nonzero error code indicates a failure, probably because the file
5207 # doesn't parse.
5208 DieWithError("gn format failed on " + gn_diff_file +
5209 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005210
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005211 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005212
5213
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005214@subcommand.usage('<codereview url or issue id>')
5215def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005216 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005217 _, args = parser.parse_args(args)
5218
5219 if len(args) != 1:
5220 parser.print_help()
5221 return 1
5222
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005223 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005224 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005225 parser.print_help()
5226 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005227 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005228
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005229 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005230 output = RunGit(['config', '--local', '--get-regexp',
5231 r'branch\..*\.%s' % issueprefix],
5232 error_ok=True)
5233 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005234 if issue == target_issue:
5235 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005236
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005237 branches = []
5238 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005239 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005240 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005241 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005242 return 1
5243 if len(branches) == 1:
5244 RunGit(['checkout', branches[0]])
5245 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005246 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005247 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005248 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005249 which = raw_input('Choose by index: ')
5250 try:
5251 RunGit(['checkout', branches[int(which)]])
5252 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005253 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005254 return 1
5255
5256 return 0
5257
5258
maruel@chromium.org29404b52014-09-08 22:58:00 +00005259def CMDlol(parser, args):
5260 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005261 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005262 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5263 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5264 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005265 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005266 return 0
5267
5268
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005269class OptionParser(optparse.OptionParser):
5270 """Creates the option parse and add --verbose support."""
5271 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005272 optparse.OptionParser.__init__(
5273 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005274 self.add_option(
5275 '-v', '--verbose', action='count', default=0,
5276 help='Use 2 times for more debugging info')
5277
5278 def parse_args(self, args=None, values=None):
5279 options, args = optparse.OptionParser.parse_args(self, args, values)
5280 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5281 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5282 return options, args
5283
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005285def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005286 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005287 print('\nYour python version %s is unsupported, please upgrade.\n' %
5288 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005289 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005290
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005291 # Reload settings.
5292 global settings
5293 settings = Settings()
5294
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005295 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005296 dispatcher = subcommand.CommandDispatcher(__name__)
5297 try:
5298 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005299 except auth.AuthenticationError as e:
5300 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005301 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005302 if e.code != 500:
5303 raise
5304 DieWithError(
5305 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5306 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005307 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005308
5309
5310if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005311 # These affect sys.stdout so do it outside of main() to simplify mocks in
5312 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005313 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005314 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005315 try:
5316 sys.exit(main(sys.argv[1:]))
5317 except KeyboardInterrupt:
5318 sys.stderr.write('interrupted\n')
5319 sys.exit(1)