blob: 174d46b3be6f0fa740e246577b7278f63dbbde5a [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
tandriide281ae2016-10-12 06:02:30 -0700321def _trigger_try_jobs(auth_config, changelist, masters, options,
322 category='git_cl_try', patchset=None):
323 assert changelist.GetIssue(), 'CL must be uploaded first'
324 codereview_url = changelist.GetCodereviewServer()
325 assert codereview_url, 'CL must be uploaded first'
326 patchset = patchset or changelist.GetMostRecentPatchset()
327 assert patchset, 'CL must be uploaded first'
328
329 codereview_host = urlparse.urlparse(codereview_url).hostname
330 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331 http = authenticator.authorize(httplib2.Http())
332 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700333
334 # TODO(tandrii): consider caching Gerrit CL details just like
335 # _RietveldChangelistImpl does, then caching values in these two variables
336 # won't be necessary.
337 owner_email = changelist.GetIssueOwner()
338 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339
340 buildbucket_put_url = (
341 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000342 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700343 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
344 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
345 hostname=codereview_host,
346 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700348 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349
350 batch_req_body = {'builds': []}
351 print_text = []
352 print_text.append('Tried jobs on:')
353 for master, builders_and_tests in sorted(masters.iteritems()):
354 print_text.append('Master: %s' % master)
355 bucket = _prefix_master(master)
356 for builder, tests in sorted(builders_and_tests.iteritems()):
357 print_text.append(' %s: %s' % (builder, tests))
358 parameters = {
359 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000360 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700361 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000362 'revision': options.revision,
363 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000364 'properties': {
365 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700366 'issue': changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367 'master': master,
tandriide281ae2016-10-12 06:02:30 -0700368 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369 'patch_storage': 'rietveld',
370 'patchset': patchset,
371 'reason': options.name,
tandriide281ae2016-10-12 06:02:30 -0700372 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373 },
374 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000375 if 'presubmit' in builder.lower():
376 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000377 if tests:
378 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700379 if extra_properties:
380 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000381 if options.clobber:
382 parameters['properties']['clobber'] = True
383 batch_req_body['builds'].append(
384 {
385 'bucket': bucket,
386 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000388 'tags': ['builder:%s' % builder,
389 'buildset:%s' % buildset,
390 'master:%s' % master,
391 'user_agent:git_cl_try']
392 }
393 )
394
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000395 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700396 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000397 http,
398 buildbucket_put_url,
399 'PUT',
400 body=json.dumps(batch_req_body),
401 headers={'Content-Type': 'application/json'}
402 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000403 print_text.append('To see results here, run: git cl try-results')
404 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700405 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000406
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000407
tandrii221ab252016-10-06 08:12:04 -0700408def fetch_try_jobs(auth_config, changelist, buildbucket_host,
409 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700410 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000411
qyearsley53f48a12016-09-01 10:45:13 -0700412 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000413 """
tandrii221ab252016-10-06 08:12:04 -0700414 assert buildbucket_host
415 assert changelist.GetIssue(), 'CL must be uploaded first'
416 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
417 patchset = patchset or changelist.GetMostRecentPatchset()
418 assert patchset, 'CL must be uploaded first'
419
420 codereview_url = changelist.GetCodereviewServer()
421 codereview_host = urlparse.urlparse(codereview_url).hostname
422 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000423 if authenticator.has_cached_credentials():
424 http = authenticator.authorize(httplib2.Http())
425 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700426 print('Warning: Some results might be missing because %s' %
427 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700428 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000429 http = httplib2.Http()
430
431 http.force_exception_to_status_code = True
432
tandrii221ab252016-10-06 08:12:04 -0700433 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
434 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
435 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000436 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700437 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000438 params = {'tag': 'buildset:%s' % buildset}
439
440 builds = {}
441 while True:
442 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700443 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000444 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700445 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000446 for build in content.get('builds', []):
447 builds[build['id']] = build
448 if 'next_cursor' in content:
449 params['start_cursor'] = content['next_cursor']
450 else:
451 break
452 return builds
453
454
qyearsleyeab3c042016-08-24 09:18:28 -0700455def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000456 """Prints nicely result of fetch_try_jobs."""
457 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700458 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 return
460
461 # Make a copy, because we'll be modifying builds dictionary.
462 builds = builds.copy()
463 builder_names_cache = {}
464
465 def get_builder(b):
466 try:
467 return builder_names_cache[b['id']]
468 except KeyError:
469 try:
470 parameters = json.loads(b['parameters_json'])
471 name = parameters['builder_name']
472 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700473 print('WARNING: failed to get builder name for build %s: %s' % (
474 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475 name = None
476 builder_names_cache[b['id']] = name
477 return name
478
479 def get_bucket(b):
480 bucket = b['bucket']
481 if bucket.startswith('master.'):
482 return bucket[len('master.'):]
483 return bucket
484
485 if options.print_master:
486 name_fmt = '%%-%ds %%-%ds' % (
487 max(len(str(get_bucket(b))) for b in builds.itervalues()),
488 max(len(str(get_builder(b))) for b in builds.itervalues()))
489 def get_name(b):
490 return name_fmt % (get_bucket(b), get_builder(b))
491 else:
492 name_fmt = '%%-%ds' % (
493 max(len(str(get_builder(b))) for b in builds.itervalues()))
494 def get_name(b):
495 return name_fmt % get_builder(b)
496
497 def sort_key(b):
498 return b['status'], b.get('result'), get_name(b), b.get('url')
499
500 def pop(title, f, color=None, **kwargs):
501 """Pop matching builds from `builds` dict and print them."""
502
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000503 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 colorize = str
505 else:
506 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
507
508 result = []
509 for b in builds.values():
510 if all(b.get(k) == v for k, v in kwargs.iteritems()):
511 builds.pop(b['id'])
512 result.append(b)
513 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700514 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000515 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700516 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517
518 total = len(builds)
519 pop(status='COMPLETED', result='SUCCESS',
520 title='Successes:', color=Fore.GREEN,
521 f=lambda b: (get_name(b), b.get('url')))
522 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
523 title='Infra Failures:', color=Fore.MAGENTA,
524 f=lambda b: (get_name(b), b.get('url')))
525 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
526 title='Failures:', color=Fore.RED,
527 f=lambda b: (get_name(b), b.get('url')))
528 pop(status='COMPLETED', result='CANCELED',
529 title='Canceled:', color=Fore.MAGENTA,
530 f=lambda b: (get_name(b),))
531 pop(status='COMPLETED', result='FAILURE',
532 failure_reason='INVALID_BUILD_DEFINITION',
533 title='Wrong master/builder name:', color=Fore.MAGENTA,
534 f=lambda b: (get_name(b),))
535 pop(status='COMPLETED', result='FAILURE',
536 title='Other failures:',
537 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
538 pop(status='COMPLETED',
539 title='Other finished:',
540 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
541 pop(status='STARTED',
542 title='Started:', color=Fore.YELLOW,
543 f=lambda b: (get_name(b), b.get('url')))
544 pop(status='SCHEDULED',
545 title='Scheduled:',
546 f=lambda b: (get_name(b), 'id=%s' % b['id']))
547 # The last section is just in case buildbucket API changes OR there is a bug.
548 pop(title='Other:',
549 f=lambda b: (get_name(b), 'id=%s' % b['id']))
550 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700551 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552
553
qyearsley53f48a12016-09-01 10:45:13 -0700554def write_try_results_json(output_file, builds):
555 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
556
557 The input |builds| dict is assumed to be generated by Buildbucket.
558 Buildbucket documentation: http://goo.gl/G0s101
559 """
560
561 def convert_build_dict(build):
562 return {
563 'buildbucket_id': build.get('id'),
564 'status': build.get('status'),
565 'result': build.get('result'),
566 'bucket': build.get('bucket'),
567 'builder_name': json.loads(
568 build.get('parameters_json', '{}')).get('builder_name'),
569 'failure_reason': build.get('failure_reason'),
570 'url': build.get('url'),
571 }
572
573 converted = []
574 for _, build in sorted(builds.items()):
575 converted.append(convert_build_dict(build))
576 write_json(output_file, converted)
577
578
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000579def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
580 """Return the corresponding git ref if |base_url| together with |glob_spec|
581 matches the full |url|.
582
583 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
584 """
585 fetch_suburl, as_ref = glob_spec.split(':')
586 if allow_wildcards:
587 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
588 if glob_match:
589 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
590 # "branches/{472,597,648}/src:refs/remotes/svn/*".
591 branch_re = re.escape(base_url)
592 if glob_match.group(1):
593 branch_re += '/' + re.escape(glob_match.group(1))
594 wildcard = glob_match.group(2)
595 if wildcard == '*':
596 branch_re += '([^/]*)'
597 else:
598 # Escape and replace surrounding braces with parentheses and commas
599 # with pipe symbols.
600 wildcard = re.escape(wildcard)
601 wildcard = re.sub('^\\\\{', '(', wildcard)
602 wildcard = re.sub('\\\\,', '|', wildcard)
603 wildcard = re.sub('\\\\}$', ')', wildcard)
604 branch_re += wildcard
605 if glob_match.group(3):
606 branch_re += re.escape(glob_match.group(3))
607 match = re.match(branch_re, url)
608 if match:
609 return re.sub('\*$', match.group(1), as_ref)
610
611 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
612 if fetch_suburl:
613 full_url = base_url + '/' + fetch_suburl
614 else:
615 full_url = base_url
616 if full_url == url:
617 return as_ref
618 return None
619
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000620
iannucci@chromium.org79540052012-10-19 23:15:26 +0000621def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000622 """Prints statistics about the change to the user."""
623 # --no-ext-diff is broken in some versions of Git, so try to work around
624 # this by overriding the environment (but there is still a problem if the
625 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000627 if 'GIT_EXTERNAL_DIFF' in env:
628 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000629
630 if find_copies:
631 similarity_options = ['--find-copies-harder', '-l100000',
632 '-C%s' % similarity]
633 else:
634 similarity_options = ['-M%s' % similarity]
635
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000636 try:
637 stdout = sys.stdout.fileno()
638 except AttributeError:
639 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000640 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000641 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000642 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000643 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000644
645
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000646class BuildbucketResponseException(Exception):
647 pass
648
649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650class Settings(object):
651 def __init__(self):
652 self.default_server = None
653 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000654 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 self.is_git_svn = None
656 self.svn_branch = None
657 self.tree_status_url = None
658 self.viewvc_url = None
659 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000660 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000661 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000662 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000663 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000664 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000665 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000666 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700667 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 def LazyUpdateIfNeeded(self):
670 """Updates the settings from a codereview.settings file, if available."""
671 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000672 # The only value that actually changes the behavior is
673 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000674 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000675 error_ok=True
676 ).strip().lower()
677
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000679 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 LoadCodereviewSettingsFromFile(cr_settings_file)
681 self.updated = True
682
683 def GetDefaultServerUrl(self, error_ok=False):
684 if not self.default_server:
685 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000686 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000687 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688 if error_ok:
689 return self.default_server
690 if not self.default_server:
691 error_message = ('Could not find settings file. You must configure '
692 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000693 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000694 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 return self.default_server
696
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000697 @staticmethod
698 def GetRelativeRoot():
699 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000700
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000701 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000702 if self.root is None:
703 self.root = os.path.abspath(self.GetRelativeRoot())
704 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000705
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000706 def GetGitMirror(self, remote='origin'):
707 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000708 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000709 if not os.path.isdir(local_url):
710 return None
711 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
712 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
713 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
714 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
715 if mirror.exists():
716 return mirror
717 return None
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 def GetIsGitSvn(self):
720 """Return true if this repo looks like it's using git-svn."""
721 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000722 if self.GetPendingRefPrefix():
723 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
724 self.is_git_svn = False
725 else:
726 # If you have any "svn-remote.*" config keys, we think you're using svn.
727 self.is_git_svn = RunGitWithCode(
728 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 return self.is_git_svn
730
731 def GetSVNBranch(self):
732 if self.svn_branch is None:
733 if not self.GetIsGitSvn():
734 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
735
736 # Try to figure out which remote branch we're based on.
737 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000738 # 1) iterate through our branch history and find the svn URL.
739 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
741 # regexp matching the git-svn line that contains the URL.
742 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
743
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000744 # We don't want to go through all of history, so read a line from the
745 # pipe at a time.
746 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000747 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000748 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
749 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000750 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000751 for line in proc.stdout:
752 match = git_svn_re.match(line)
753 if match:
754 url = match.group(1)
755 proc.stdout.close() # Cut pipe.
756 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000758 if url:
759 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
760 remotes = RunGit(['config', '--get-regexp',
761 r'^svn-remote\..*\.url']).splitlines()
762 for remote in remotes:
763 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000765 remote = match.group(1)
766 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000767 rewrite_root = RunGit(
768 ['config', 'svn-remote.%s.rewriteRoot' % remote],
769 error_ok=True).strip()
770 if rewrite_root:
771 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000772 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000773 ['config', 'svn-remote.%s.fetch' % remote],
774 error_ok=True).strip()
775 if fetch_spec:
776 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
777 if self.svn_branch:
778 break
779 branch_spec = RunGit(
780 ['config', 'svn-remote.%s.branches' % remote],
781 error_ok=True).strip()
782 if branch_spec:
783 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
784 if self.svn_branch:
785 break
786 tag_spec = RunGit(
787 ['config', 'svn-remote.%s.tags' % remote],
788 error_ok=True).strip()
789 if tag_spec:
790 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
791 if self.svn_branch:
792 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793
794 if not self.svn_branch:
795 DieWithError('Can\'t guess svn branch -- try specifying it on the '
796 'command line')
797
798 return self.svn_branch
799
800 def GetTreeStatusUrl(self, error_ok=False):
801 if not self.tree_status_url:
802 error_message = ('You must configure your tree status URL by running '
803 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 self.tree_status_url = self._GetRietveldConfig(
805 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806 return self.tree_status_url
807
808 def GetViewVCUrl(self):
809 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 return self.viewvc_url
812
rmistry@google.com90752582014-01-14 21:04:50 +0000813 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000814 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000815
rmistry@google.com78948ed2015-07-08 23:09:57 +0000816 def GetIsSkipDependencyUpload(self, branch_name):
817 """Returns true if specified branch should skip dep uploads."""
818 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
819 error_ok=True)
820
rmistry@google.com5626a922015-02-26 14:03:30 +0000821 def GetRunPostUploadHook(self):
822 run_post_upload_hook = self._GetRietveldConfig(
823 'run-post-upload-hook', error_ok=True)
824 return run_post_upload_hook == "True"
825
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000826 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000827 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000828
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000829 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000830 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000831
ukai@chromium.orge8077812012-02-03 03:41:46 +0000832 def GetIsGerrit(self):
833 """Return true if this repo is assosiated with gerrit code review system."""
834 if self.is_gerrit is None:
835 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
836 return self.is_gerrit
837
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000838 def GetSquashGerritUploads(self):
839 """Return true if uploads to Gerrit should be squashed by default."""
840 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700841 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
842 if self.squash_gerrit_uploads is None:
843 # Default is squash now (http://crbug.com/611892#c23).
844 self.squash_gerrit_uploads = not (
845 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
846 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000847 return self.squash_gerrit_uploads
848
tandriia60502f2016-06-20 02:01:53 -0700849 def GetSquashGerritUploadsOverride(self):
850 """Return True or False if codereview.settings should be overridden.
851
852 Returns None if no override has been defined.
853 """
854 # See also http://crbug.com/611892#c23
855 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
856 error_ok=True).strip()
857 if result == 'true':
858 return True
859 if result == 'false':
860 return False
861 return None
862
tandrii@chromium.org28253532016-04-14 13:46:56 +0000863 def GetGerritSkipEnsureAuthenticated(self):
864 """Return True if EnsureAuthenticated should not be done for Gerrit
865 uploads."""
866 if self.gerrit_skip_ensure_authenticated is None:
867 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000868 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000869 error_ok=True).strip() == 'true')
870 return self.gerrit_skip_ensure_authenticated
871
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000872 def GetGitEditor(self):
873 """Return the editor specified in the git config, or None if none is."""
874 if self.git_editor is None:
875 self.git_editor = self._GetConfig('core.editor', error_ok=True)
876 return self.git_editor or None
877
thestig@chromium.org44202a22014-03-11 19:22:18 +0000878 def GetLintRegex(self):
879 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
880 DEFAULT_LINT_REGEX)
881
882 def GetLintIgnoreRegex(self):
883 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
884 DEFAULT_LINT_IGNORE_REGEX)
885
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000886 def GetProject(self):
887 if not self.project:
888 self.project = self._GetRietveldConfig('project', error_ok=True)
889 return self.project
890
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000891 def GetForceHttpsCommitUrl(self):
892 if not self.force_https_commit_url:
893 self.force_https_commit_url = self._GetRietveldConfig(
894 'force-https-commit-url', error_ok=True)
895 return self.force_https_commit_url
896
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000897 def GetPendingRefPrefix(self):
898 if not self.pending_ref_prefix:
899 self.pending_ref_prefix = self._GetRietveldConfig(
900 'pending-ref-prefix', error_ok=True)
901 return self.pending_ref_prefix
902
tandriif46c20f2016-09-14 06:17:05 -0700903 def GetHasGitNumberFooter(self):
904 # TODO(tandrii): this has to be removed after Rietveld is read-only.
905 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
906 if not self.git_number_footer:
907 self.git_number_footer = self._GetRietveldConfig(
908 'git-number-footer', error_ok=True)
909 return self.git_number_footer
910
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000911 def _GetRietveldConfig(self, param, **kwargs):
912 return self._GetConfig('rietveld.' + param, **kwargs)
913
rmistry@google.com78948ed2015-07-08 23:09:57 +0000914 def _GetBranchConfig(self, branch_name, param, **kwargs):
915 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917 def _GetConfig(self, param, **kwargs):
918 self.LazyUpdateIfNeeded()
919 return RunGit(['config', param], **kwargs).strip()
920
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922def ShortBranchName(branch):
923 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924 return branch.replace('refs/heads/', '', 1)
925
926
927def GetCurrentBranchRef():
928 """Returns branch ref (e.g., refs/heads/master) or None."""
929 return RunGit(['symbolic-ref', 'HEAD'],
930 stderr=subprocess2.VOID, error_ok=True).strip() or None
931
932
933def GetCurrentBranch():
934 """Returns current branch or None.
935
936 For refs/heads/* branches, returns just last part. For others, full ref.
937 """
938 branchref = GetCurrentBranchRef()
939 if branchref:
940 return ShortBranchName(branchref)
941 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
943
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000944class _CQState(object):
945 """Enum for states of CL with respect to Commit Queue."""
946 NONE = 'none'
947 DRY_RUN = 'dry_run'
948 COMMIT = 'commit'
949
950 ALL_STATES = [NONE, DRY_RUN, COMMIT]
951
952
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000953class _ParsedIssueNumberArgument(object):
954 def __init__(self, issue=None, patchset=None, hostname=None):
955 self.issue = issue
956 self.patchset = patchset
957 self.hostname = hostname
958
959 @property
960 def valid(self):
961 return self.issue is not None
962
963
964class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
965 def __init__(self, *args, **kwargs):
966 self.patch_url = kwargs.pop('patch_url', None)
967 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
968
969
970def ParseIssueNumberArgument(arg):
971 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
972 fail_result = _ParsedIssueNumberArgument()
973
974 if arg.isdigit():
975 return _ParsedIssueNumberArgument(issue=int(arg))
976 if not arg.startswith('http'):
977 return fail_result
978 url = gclient_utils.UpgradeToHttps(arg)
979 try:
980 parsed_url = urlparse.urlparse(url)
981 except ValueError:
982 return fail_result
983 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
984 tmp = cls.ParseIssueURL(parsed_url)
985 if tmp is not None:
986 return tmp
987 return fail_result
988
989
tandriic2405f52016-10-10 08:13:15 -0700990class GerritIssueNotExists(Exception):
991 def __init__(self, issue, url):
992 self.issue = issue
993 self.url = url
994 super(GerritIssueNotExists, self).__init__()
995
996 def __str__(self):
997 return 'issue %s at %s does not exist or you have no access to it' % (
998 self.issue, self.url)
999
1000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001002 """Changelist works with one changelist in local branch.
1003
1004 Supports two codereview backends: Rietveld or Gerrit, selected at object
1005 creation.
1006
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001007 Notes:
1008 * Not safe for concurrent multi-{thread,process} use.
1009 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001010 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001011 """
1012
1013 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1014 """Create a new ChangeList instance.
1015
1016 If issue is given, the codereview must be given too.
1017
1018 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1019 Otherwise, it's decided based on current configuration of the local branch,
1020 with default being 'rietveld' for backwards compatibility.
1021 See _load_codereview_impl for more details.
1022
1023 **kwargs will be passed directly to codereview implementation.
1024 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001026 global settings
1027 if not settings:
1028 # Happens when git_cl.py is used as a utility library.
1029 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001030
1031 if issue:
1032 assert codereview, 'codereview must be known, if issue is known'
1033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 self.branchref = branchref
1035 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001036 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 self.branch = ShortBranchName(self.branchref)
1038 else:
1039 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001041 self.lookedup_issue = False
1042 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 self.has_description = False
1044 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001045 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001047 self.cc = None
1048 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001049 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001050
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001051 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001052 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001053 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001054 assert self._codereview_impl
1055 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001056
1057 def _load_codereview_impl(self, codereview=None, **kwargs):
1058 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001059 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1060 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1061 self._codereview = codereview
1062 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001063 return
1064
1065 # Automatic selection based on issue number set for a current branch.
1066 # Rietveld takes precedence over Gerrit.
1067 assert not self.issue
1068 # Whether we find issue or not, we are doing the lookup.
1069 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001070 if self.GetBranch():
1071 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1072 issue = _git_get_branch_config_value(
1073 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1074 if issue:
1075 self._codereview = codereview
1076 self._codereview_impl = cls(self, **kwargs)
1077 self.issue = int(issue)
1078 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001079
1080 # No issue is set for this branch, so decide based on repo-wide settings.
1081 return self._load_codereview_impl(
1082 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1083 **kwargs)
1084
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001085 def IsGerrit(self):
1086 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001087
1088 def GetCCList(self):
1089 """Return the users cc'd on this CL.
1090
agable92bec4f2016-08-24 09:27:27 -07001091 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001092 """
1093 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001094 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001095 more_cc = ','.join(self.watchers)
1096 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1097 return self.cc
1098
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001099 def GetCCListWithoutDefault(self):
1100 """Return the users cc'd on this CL excluding default ones."""
1101 if self.cc is None:
1102 self.cc = ','.join(self.watchers)
1103 return self.cc
1104
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001105 def SetWatchers(self, watchers):
1106 """Set the list of email addresses that should be cc'd based on the changed
1107 files in this CL.
1108 """
1109 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110
1111 def GetBranch(self):
1112 """Returns the short branch name, e.g. 'master'."""
1113 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001115 if not branchref:
1116 return None
1117 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 self.branch = ShortBranchName(self.branchref)
1119 return self.branch
1120
1121 def GetBranchRef(self):
1122 """Returns the full branch name, e.g. 'refs/heads/master'."""
1123 self.GetBranch() # Poke the lazy loader.
1124 return self.branchref
1125
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001126 def ClearBranch(self):
1127 """Clears cached branch data of this object."""
1128 self.branch = self.branchref = None
1129
tandrii5d48c322016-08-18 16:19:37 -07001130 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1131 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1132 kwargs['branch'] = self.GetBranch()
1133 return _git_get_branch_config_value(key, default, **kwargs)
1134
1135 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1136 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1137 assert self.GetBranch(), (
1138 'this CL must have an associated branch to %sset %s%s' %
1139 ('un' if value is None else '',
1140 key,
1141 '' if value is None else ' to %r' % value))
1142 kwargs['branch'] = self.GetBranch()
1143 return _git_set_branch_config_value(key, value, **kwargs)
1144
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001145 @staticmethod
1146 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001147 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 e.g. 'origin', 'refs/heads/master'
1149 """
1150 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001151 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001154 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001156 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1157 error_ok=True).strip()
1158 if upstream_branch:
1159 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001161 # Fall back on trying a git-svn upstream branch.
1162 if settings.GetIsGitSvn():
1163 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001165 # Else, try to guess the origin remote.
1166 remote_branches = RunGit(['branch', '-r']).split()
1167 if 'origin/master' in remote_branches:
1168 # Fall back on origin/master if it exits.
1169 remote = 'origin'
1170 upstream_branch = 'refs/heads/master'
1171 elif 'origin/trunk' in remote_branches:
1172 # Fall back on origin/trunk if it exists. Generally a shared
1173 # git-svn clone
1174 remote = 'origin'
1175 upstream_branch = 'refs/heads/trunk'
1176 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001177 DieWithError(
1178 'Unable to determine default branch to diff against.\n'
1179 'Either pass complete "git diff"-style arguments, like\n'
1180 ' git cl upload origin/master\n'
1181 'or verify this branch is set up to track another \n'
1182 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183
1184 return remote, upstream_branch
1185
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001186 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001187 upstream_branch = self.GetUpstreamBranch()
1188 if not BranchExists(upstream_branch):
1189 DieWithError('The upstream for the current branch (%s) does not exist '
1190 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001191 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001192 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001193
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 def GetUpstreamBranch(self):
1195 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001196 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001198 upstream_branch = upstream_branch.replace('refs/heads/',
1199 'refs/remotes/%s/' % remote)
1200 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1201 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 self.upstream_branch = upstream_branch
1203 return self.upstream_branch
1204
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001205 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001206 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, branch = None, self.GetBranch()
1208 seen_branches = set()
1209 while branch not in seen_branches:
1210 seen_branches.add(branch)
1211 remote, branch = self.FetchUpstreamTuple(branch)
1212 branch = ShortBranchName(branch)
1213 if remote != '.' or branch.startswith('refs/remotes'):
1214 break
1215 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001216 remotes = RunGit(['remote'], error_ok=True).split()
1217 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001218 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001219 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001221 logging.warning('Could not determine which remote this change is '
1222 'associated with, so defaulting to "%s". This may '
1223 'not be what you want. You may prevent this message '
1224 'by running "git svn info" as documented here: %s',
1225 self._remote,
1226 GIT_INSTRUCTIONS_URL)
1227 else:
1228 logging.warn('Could not determine which remote this change is '
1229 'associated with. You may prevent this message by '
1230 'running "git svn info" as documented here: %s',
1231 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001232 branch = 'HEAD'
1233 if branch.startswith('refs/remotes'):
1234 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001235 elif branch.startswith('refs/branch-heads/'):
1236 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 else:
1238 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001239 return self._remote
1240
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 def GitSanityChecks(self, upstream_git_obj):
1242 """Checks git repo status and ensures diff is from local commits."""
1243
sbc@chromium.org79706062015-01-14 21:18:12 +00001244 if upstream_git_obj is None:
1245 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001246 print('ERROR: unable to determine current branch (detached HEAD?)',
1247 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001248 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001249 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001250 return False
1251
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 # Verify the commit we're diffing against is in our current branch.
1253 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1254 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1255 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001256 print('ERROR: %s is not in the current branch. You may need to rebase '
1257 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 return False
1259
1260 # List the commits inside the diff, and verify they are all local.
1261 commits_in_diff = RunGit(
1262 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1263 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1264 remote_branch = remote_branch.strip()
1265 if code != 0:
1266 _, remote_branch = self.GetRemoteBranch()
1267
1268 commits_in_remote = RunGit(
1269 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1270
1271 common_commits = set(commits_in_diff) & set(commits_in_remote)
1272 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001273 print('ERROR: Your diff contains %d commits already in %s.\n'
1274 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1275 'the diff. If you are using a custom git flow, you can override'
1276 ' the reference used for this check with "git config '
1277 'gitcl.remotebranch <git-ref>".' % (
1278 len(common_commits), remote_branch, upstream_git_obj),
1279 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001280 return False
1281 return True
1282
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001283 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001284 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001285
1286 Returns None if it is not set.
1287 """
tandrii5d48c322016-08-18 16:19:37 -07001288 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001289
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001290 def GetGitSvnRemoteUrl(self):
1291 """Return the configured git-svn remote URL parsed from git svn info.
1292
1293 Returns None if it is not set.
1294 """
1295 # URL is dependent on the current directory.
1296 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1297 if data:
1298 keys = dict(line.split(': ', 1) for line in data.splitlines()
1299 if ': ' in line)
1300 return keys.get('URL', None)
1301 return None
1302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 def GetRemoteUrl(self):
1304 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1305
1306 Returns None if there is no remote.
1307 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001309 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1310
1311 # If URL is pointing to a local directory, it is probably a git cache.
1312 if os.path.isdir(url):
1313 url = RunGit(['config', 'remote.%s.url' % remote],
1314 error_ok=True,
1315 cwd=url).strip()
1316 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001318 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001319 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001320 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001321 self.issue = self._GitGetBranchConfigValue(
1322 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001323 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324 return self.issue
1325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 def GetIssueURL(self):
1327 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001328 issue = self.GetIssue()
1329 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001330 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001331 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
1333 def GetDescription(self, pretty=False):
1334 if not self.has_description:
1335 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001336 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 self.has_description = True
1338 if pretty:
1339 wrapper = textwrap.TextWrapper()
1340 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1341 return wrapper.fill(self.description)
1342 return self.description
1343
1344 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001345 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001346 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001347 self.patchset = self._GitGetBranchConfigValue(
1348 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001349 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 return self.patchset
1351
1352 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001353 """Set this branch's patchset. If patchset=0, clears the patchset."""
1354 assert self.GetBranch()
1355 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001356 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001357 else:
1358 self.patchset = int(patchset)
1359 self._GitSetBranchConfigValue(
1360 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001362 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001363 """Set this branch's issue. If issue isn't given, clears the issue."""
1364 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001366 issue = int(issue)
1367 self._GitSetBranchConfigValue(
1368 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001369 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001370 codereview_server = self._codereview_impl.GetCodereviewServer()
1371 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001372 self._GitSetBranchConfigValue(
1373 self._codereview_impl.CodereviewServerConfigKey(),
1374 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 else:
tandrii5d48c322016-08-18 16:19:37 -07001376 # Reset all of these just to be clean.
1377 reset_suffixes = [
1378 'last-upload-hash',
1379 self._codereview_impl.IssueConfigKey(),
1380 self._codereview_impl.PatchsetConfigKey(),
1381 self._codereview_impl.CodereviewServerConfigKey(),
1382 ] + self._PostUnsetIssueProperties()
1383 for prop in reset_suffixes:
1384 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001385 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001386 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387
dnjba1b0f32016-09-02 12:37:42 -07001388 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001389 if not self.GitSanityChecks(upstream_branch):
1390 DieWithError('\nGit sanity check failure')
1391
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001392 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001393 if not root:
1394 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001395 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001396
1397 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001398 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001399 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001400 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001401 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001402 except subprocess2.CalledProcessError:
1403 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001404 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001405 'This branch probably doesn\'t exist anymore. To reset the\n'
1406 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001407 ' git branch --set-upstream-to origin/master %s\n'
1408 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001409 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001410
maruel@chromium.org52424302012-08-29 15:14:30 +00001411 issue = self.GetIssue()
1412 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001413 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001414 description = self.GetDescription()
1415 else:
1416 # If the change was never uploaded, use the log messages of all commits
1417 # up to the branch point, as git cl upload will prefill the description
1418 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001419 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1420 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001421
1422 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001423 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001424 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001425 name,
1426 description,
1427 absroot,
1428 files,
1429 issue,
1430 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001431 author,
1432 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001433
dsansomee2d6fd92016-09-08 00:10:47 -07001434 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001435 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001436 return self._codereview_impl.UpdateDescriptionRemote(
1437 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001438
1439 def RunHook(self, committing, may_prompt, verbose, change):
1440 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1441 try:
1442 return presubmit_support.DoPresubmitChecks(change, committing,
1443 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1444 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001445 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1446 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001447 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001448 DieWithError(
1449 ('%s\nMaybe your depot_tools is out of date?\n'
1450 'If all fails, contact maruel@') % e)
1451
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001452 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1453 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001454 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1455 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001456 else:
1457 # Assume url.
1458 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1459 urlparse.urlparse(issue_arg))
1460 if not parsed_issue_arg or not parsed_issue_arg.valid:
1461 DieWithError('Failed to parse issue argument "%s". '
1462 'Must be an issue number or a valid URL.' % issue_arg)
1463 return self._codereview_impl.CMDPatchWithParsedIssue(
1464 parsed_issue_arg, reject, nocommit, directory)
1465
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001466 def CMDUpload(self, options, git_diff_args, orig_args):
1467 """Uploads a change to codereview."""
1468 if git_diff_args:
1469 # TODO(ukai): is it ok for gerrit case?
1470 base_branch = git_diff_args[0]
1471 else:
1472 if self.GetBranch() is None:
1473 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1474
1475 # Default to diffing against common ancestor of upstream branch
1476 base_branch = self.GetCommonAncestorWithUpstream()
1477 git_diff_args = [base_branch, 'HEAD']
1478
1479 # Make sure authenticated to codereview before running potentially expensive
1480 # hooks. It is a fast, best efforts check. Codereview still can reject the
1481 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001482 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001483
1484 # Apply watchlists on upload.
1485 change = self.GetChange(base_branch, None)
1486 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1487 files = [f.LocalPath() for f in change.AffectedFiles()]
1488 if not options.bypass_watchlists:
1489 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1490
1491 if not options.bypass_hooks:
1492 if options.reviewers or options.tbr_owners:
1493 # Set the reviewer list now so that presubmit checks can access it.
1494 change_description = ChangeDescription(change.FullDescriptionText())
1495 change_description.update_reviewers(options.reviewers,
1496 options.tbr_owners,
1497 change)
1498 change.SetDescriptionText(change_description.description)
1499 hook_results = self.RunHook(committing=False,
1500 may_prompt=not options.force,
1501 verbose=options.verbose,
1502 change=change)
1503 if not hook_results.should_continue():
1504 return 1
1505 if not options.reviewers and hook_results.reviewers:
1506 options.reviewers = hook_results.reviewers.split(',')
1507
1508 if self.GetIssue():
1509 latest_patchset = self.GetMostRecentPatchset()
1510 local_patchset = self.GetPatchset()
1511 if (latest_patchset and local_patchset and
1512 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001513 print('The last upload made from this repository was patchset #%d but '
1514 'the most recent patchset on the server is #%d.'
1515 % (local_patchset, latest_patchset))
1516 print('Uploading will still work, but if you\'ve uploaded to this '
1517 'issue from another machine or branch the patch you\'re '
1518 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001519 ask_for_data('About to upload; enter to confirm.')
1520
1521 print_stats(options.similarity, options.find_copies, git_diff_args)
1522 ret = self.CMDUploadChange(options, git_diff_args, change)
1523 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001524 if options.use_commit_queue:
1525 self.SetCQState(_CQState.COMMIT)
1526 elif options.cq_dry_run:
1527 self.SetCQState(_CQState.DRY_RUN)
1528
tandrii5d48c322016-08-18 16:19:37 -07001529 _git_set_branch_config_value('last-upload-hash',
1530 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001531 # Run post upload hooks, if specified.
1532 if settings.GetRunPostUploadHook():
1533 presubmit_support.DoPostUploadExecuter(
1534 change,
1535 self,
1536 settings.GetRoot(),
1537 options.verbose,
1538 sys.stdout)
1539
1540 # Upload all dependencies if specified.
1541 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001542 print()
1543 print('--dependencies has been specified.')
1544 print('All dependent local branches will be re-uploaded.')
1545 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001546 # Remove the dependencies flag from args so that we do not end up in a
1547 # loop.
1548 orig_args.remove('--dependencies')
1549 ret = upload_branch_deps(self, orig_args)
1550 return ret
1551
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001552 def SetCQState(self, new_state):
1553 """Update the CQ state for latest patchset.
1554
1555 Issue must have been already uploaded and known.
1556 """
1557 assert new_state in _CQState.ALL_STATES
1558 assert self.GetIssue()
1559 return self._codereview_impl.SetCQState(new_state)
1560
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561 # Forward methods to codereview specific implementation.
1562
1563 def CloseIssue(self):
1564 return self._codereview_impl.CloseIssue()
1565
1566 def GetStatus(self):
1567 return self._codereview_impl.GetStatus()
1568
1569 def GetCodereviewServer(self):
1570 return self._codereview_impl.GetCodereviewServer()
1571
tandriide281ae2016-10-12 06:02:30 -07001572 def GetIssueOwner(self):
1573 """Get owner from codereview, which may differ from this checkout."""
1574 return self._codereview_impl.GetIssueOwner()
1575
1576 def GetIssueProject(self):
1577 """Get project from codereview, which may differ from what this
1578 checkout's codereview.settings or gerrit project URL say.
1579 """
1580 return self._codereview_impl.GetIssueProject()
1581
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001582 def GetApprovingReviewers(self):
1583 return self._codereview_impl.GetApprovingReviewers()
1584
1585 def GetMostRecentPatchset(self):
1586 return self._codereview_impl.GetMostRecentPatchset()
1587
tandriide281ae2016-10-12 06:02:30 -07001588 def CannotTriggerTryJobReason(self):
1589 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1590 return self._codereview_impl.CannotTriggerTryJobReason()
1591
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592 def __getattr__(self, attr):
1593 # This is because lots of untested code accesses Rietveld-specific stuff
1594 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001595 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001596 # Note that child method defines __getattr__ as well, and forwards it here,
1597 # because _RietveldChangelistImpl is not cleaned up yet, and given
1598 # deprecation of Rietveld, it should probably be just removed.
1599 # Until that time, avoid infinite recursion by bypassing __getattr__
1600 # of implementation class.
1601 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001602
1603
1604class _ChangelistCodereviewBase(object):
1605 """Abstract base class encapsulating codereview specifics of a changelist."""
1606 def __init__(self, changelist):
1607 self._changelist = changelist # instance of Changelist
1608
1609 def __getattr__(self, attr):
1610 # Forward methods to changelist.
1611 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1612 # _RietveldChangelistImpl to avoid this hack?
1613 return getattr(self._changelist, attr)
1614
1615 def GetStatus(self):
1616 """Apply a rough heuristic to give a simple summary of an issue's review
1617 or CQ status, assuming adherence to a common workflow.
1618
1619 Returns None if no issue for this branch, or specific string keywords.
1620 """
1621 raise NotImplementedError()
1622
1623 def GetCodereviewServer(self):
1624 """Returns server URL without end slash, like "https://codereview.com"."""
1625 raise NotImplementedError()
1626
1627 def FetchDescription(self):
1628 """Fetches and returns description from the codereview server."""
1629 raise NotImplementedError()
1630
tandrii5d48c322016-08-18 16:19:37 -07001631 @classmethod
1632 def IssueConfigKey(cls):
1633 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001634 raise NotImplementedError()
1635
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001636 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001637 def PatchsetConfigKey(cls):
1638 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001639 raise NotImplementedError()
1640
tandrii5d48c322016-08-18 16:19:37 -07001641 @classmethod
1642 def CodereviewServerConfigKey(cls):
1643 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 raise NotImplementedError()
1645
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001646 def _PostUnsetIssueProperties(self):
1647 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001648 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001649
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001650 def GetRieveldObjForPresubmit(self):
1651 # This is an unfortunate Rietveld-embeddedness in presubmit.
1652 # For non-Rietveld codereviews, this probably should return a dummy object.
1653 raise NotImplementedError()
1654
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001655 def GetGerritObjForPresubmit(self):
1656 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1657 return None
1658
dsansomee2d6fd92016-09-08 00:10:47 -07001659 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001660 """Update the description on codereview site."""
1661 raise NotImplementedError()
1662
1663 def CloseIssue(self):
1664 """Closes the issue."""
1665 raise NotImplementedError()
1666
1667 def GetApprovingReviewers(self):
1668 """Returns a list of reviewers approving the change.
1669
1670 Note: not necessarily committers.
1671 """
1672 raise NotImplementedError()
1673
1674 def GetMostRecentPatchset(self):
1675 """Returns the most recent patchset number from the codereview site."""
1676 raise NotImplementedError()
1677
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001678 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1679 directory):
1680 """Fetches and applies the issue.
1681
1682 Arguments:
1683 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1684 reject: if True, reject the failed patch instead of switching to 3-way
1685 merge. Rietveld only.
1686 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1687 only.
1688 directory: switch to directory before applying the patch. Rietveld only.
1689 """
1690 raise NotImplementedError()
1691
1692 @staticmethod
1693 def ParseIssueURL(parsed_url):
1694 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1695 failed."""
1696 raise NotImplementedError()
1697
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001698 def EnsureAuthenticated(self, force):
1699 """Best effort check that user is authenticated with codereview server.
1700
1701 Arguments:
1702 force: whether to skip confirmation questions.
1703 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001704 raise NotImplementedError()
1705
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001706 def CMDUploadChange(self, options, args, change):
1707 """Uploads a change to codereview."""
1708 raise NotImplementedError()
1709
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001710 def SetCQState(self, new_state):
1711 """Update the CQ state for latest patchset.
1712
1713 Issue must have been already uploaded and known.
1714 """
1715 raise NotImplementedError()
1716
tandriie113dfd2016-10-11 10:20:12 -07001717 def CannotTriggerTryJobReason(self):
1718 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1719 raise NotImplementedError()
1720
tandriide281ae2016-10-12 06:02:30 -07001721 def GetIssueOwner(self):
1722 raise NotImplementedError()
1723
1724 def GetIssueProject(self):
1725 raise NotImplementedError()
1726
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727
1728class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1729 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1730 super(_RietveldChangelistImpl, self).__init__(changelist)
1731 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001732 if not rietveld_server:
1733 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734
1735 self._rietveld_server = rietveld_server
1736 self._auth_config = auth_config
1737 self._props = None
1738 self._rpc_server = None
1739
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 def GetCodereviewServer(self):
1741 if not self._rietveld_server:
1742 # If we're on a branch then get the server potentially associated
1743 # with that branch.
1744 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001745 self._rietveld_server = gclient_utils.UpgradeToHttps(
1746 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001747 if not self._rietveld_server:
1748 self._rietveld_server = settings.GetDefaultServerUrl()
1749 return self._rietveld_server
1750
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001751 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001752 """Best effort check that user is authenticated with Rietveld server."""
1753 if self._auth_config.use_oauth2:
1754 authenticator = auth.get_authenticator_for_host(
1755 self.GetCodereviewServer(), self._auth_config)
1756 if not authenticator.has_cached_credentials():
1757 raise auth.LoginRequiredError(self.GetCodereviewServer())
1758
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 def FetchDescription(self):
1760 issue = self.GetIssue()
1761 assert issue
1762 try:
1763 return self.RpcServer().get_description(issue).strip()
1764 except urllib2.HTTPError as e:
1765 if e.code == 404:
1766 DieWithError(
1767 ('\nWhile fetching the description for issue %d, received a '
1768 '404 (not found)\n'
1769 'error. It is likely that you deleted this '
1770 'issue on the server. If this is the\n'
1771 'case, please run\n\n'
1772 ' git cl issue 0\n\n'
1773 'to clear the association with the deleted issue. Then run '
1774 'this command again.') % issue)
1775 else:
1776 DieWithError(
1777 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1778 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001779 print('Warning: Failed to retrieve CL description due to network '
1780 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001781 return ''
1782
1783 def GetMostRecentPatchset(self):
1784 return self.GetIssueProperties()['patchsets'][-1]
1785
1786 def GetPatchSetDiff(self, issue, patchset):
1787 return self.RpcServer().get(
1788 '/download/issue%s_%s.diff' % (issue, patchset))
1789
1790 def GetIssueProperties(self):
1791 if self._props is None:
1792 issue = self.GetIssue()
1793 if not issue:
1794 self._props = {}
1795 else:
1796 self._props = self.RpcServer().get_issue_properties(issue, True)
1797 return self._props
1798
tandriie113dfd2016-10-11 10:20:12 -07001799 def CannotTriggerTryJobReason(self):
1800 props = self.GetIssueProperties()
1801 if not props:
1802 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1803 if props.get('closed'):
1804 return 'CL %s is closed' % self.GetIssue()
1805 if props.get('private'):
1806 return 'CL %s is private' % self.GetIssue()
1807 return None
1808
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001809 def GetApprovingReviewers(self):
1810 return get_approving_reviewers(self.GetIssueProperties())
1811
tandriide281ae2016-10-12 06:02:30 -07001812 def GetIssueOwner(self):
1813 return (self.GetIssueProperties() or {}).get('owner_email')
1814
1815 def GetIssueProject(self):
1816 return (self.GetIssueProperties() or {}).get('project')
1817
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 def AddComment(self, message):
1819 return self.RpcServer().add_comment(self.GetIssue(), message)
1820
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001821 def GetStatus(self):
1822 """Apply a rough heuristic to give a simple summary of an issue's review
1823 or CQ status, assuming adherence to a common workflow.
1824
1825 Returns None if no issue for this branch, or one of the following keywords:
1826 * 'error' - error from review tool (including deleted issues)
1827 * 'unsent' - not sent for review
1828 * 'waiting' - waiting for review
1829 * 'reply' - waiting for owner to reply to review
1830 * 'lgtm' - LGTM from at least one approved reviewer
1831 * 'commit' - in the commit queue
1832 * 'closed' - closed
1833 """
1834 if not self.GetIssue():
1835 return None
1836
1837 try:
1838 props = self.GetIssueProperties()
1839 except urllib2.HTTPError:
1840 return 'error'
1841
1842 if props.get('closed'):
1843 # Issue is closed.
1844 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001845 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001846 # Issue is in the commit queue.
1847 return 'commit'
1848
1849 try:
1850 reviewers = self.GetApprovingReviewers()
1851 except urllib2.HTTPError:
1852 return 'error'
1853
1854 if reviewers:
1855 # Was LGTM'ed.
1856 return 'lgtm'
1857
1858 messages = props.get('messages') or []
1859
tandrii9d2c7a32016-06-22 03:42:45 -07001860 # Skip CQ messages that don't require owner's action.
1861 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1862 if 'Dry run:' in messages[-1]['text']:
1863 messages.pop()
1864 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1865 # This message always follows prior messages from CQ,
1866 # so skip this too.
1867 messages.pop()
1868 else:
1869 # This is probably a CQ messages warranting user attention.
1870 break
1871
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001872 if not messages:
1873 # No message was sent.
1874 return 'unsent'
1875 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001876 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001877 return 'reply'
1878 return 'waiting'
1879
dsansomee2d6fd92016-09-08 00:10:47 -07001880 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001881 return self.RpcServer().update_description(
1882 self.GetIssue(), self.description)
1883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001885 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001887 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001888 return self.SetFlags({flag: value})
1889
1890 def SetFlags(self, flags):
1891 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001892 """
phajdan.jr68598232016-08-10 03:28:28 -07001893 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001894 try:
tandrii4b233bd2016-07-06 03:50:29 -07001895 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001896 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001897 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001898 if e.code == 404:
1899 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1900 if e.code == 403:
1901 DieWithError(
1902 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001903 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001904 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001906 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 """Returns an upload.RpcServer() to access this review's rietveld instance.
1908 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001909 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001910 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001911 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001912 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001913 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001914
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001915 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001916 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001917 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918
tandrii5d48c322016-08-18 16:19:37 -07001919 @classmethod
1920 def PatchsetConfigKey(cls):
1921 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922
tandrii5d48c322016-08-18 16:19:37 -07001923 @classmethod
1924 def CodereviewServerConfigKey(cls):
1925 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001926
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001927 def GetRieveldObjForPresubmit(self):
1928 return self.RpcServer()
1929
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001930 def SetCQState(self, new_state):
1931 props = self.GetIssueProperties()
1932 if props.get('private'):
1933 DieWithError('Cannot set-commit on private issue')
1934
1935 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001936 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001937 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001938 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001939 else:
tandrii4b233bd2016-07-06 03:50:29 -07001940 assert new_state == _CQState.DRY_RUN
1941 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001942
1943
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001944 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1945 directory):
1946 # TODO(maruel): Use apply_issue.py
1947
1948 # PatchIssue should never be called with a dirty tree. It is up to the
1949 # caller to check this, but just in case we assert here since the
1950 # consequences of the caller not checking this could be dire.
1951 assert(not git_common.is_dirty_git_tree('apply'))
1952 assert(parsed_issue_arg.valid)
1953 self._changelist.issue = parsed_issue_arg.issue
1954 if parsed_issue_arg.hostname:
1955 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1956
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001957 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1958 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001959 assert parsed_issue_arg.patchset
1960 patchset = parsed_issue_arg.patchset
1961 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1962 else:
1963 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1964 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1965
1966 # Switch up to the top-level directory, if necessary, in preparation for
1967 # applying the patch.
1968 top = settings.GetRelativeRoot()
1969 if top:
1970 os.chdir(top)
1971
1972 # Git patches have a/ at the beginning of source paths. We strip that out
1973 # with a sed script rather than the -p flag to patch so we can feed either
1974 # Git or svn-style patches into the same apply command.
1975 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1976 try:
1977 patch_data = subprocess2.check_output(
1978 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1979 except subprocess2.CalledProcessError:
1980 DieWithError('Git patch mungling failed.')
1981 logging.info(patch_data)
1982
1983 # We use "git apply" to apply the patch instead of "patch" so that we can
1984 # pick up file adds.
1985 # The --index flag means: also insert into the index (so we catch adds).
1986 cmd = ['git', 'apply', '--index', '-p0']
1987 if directory:
1988 cmd.extend(('--directory', directory))
1989 if reject:
1990 cmd.append('--reject')
1991 elif IsGitVersionAtLeast('1.7.12'):
1992 cmd.append('--3way')
1993 try:
1994 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1995 stdin=patch_data, stdout=subprocess2.VOID)
1996 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001997 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001998 return 1
1999
2000 # If we had an issue, commit the current state and register the issue.
2001 if not nocommit:
2002 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2003 'patch from issue %(i)s at patchset '
2004 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2005 % {'i': self.GetIssue(), 'p': patchset})])
2006 self.SetIssue(self.GetIssue())
2007 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002008 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002009 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002010 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002011 return 0
2012
2013 @staticmethod
2014 def ParseIssueURL(parsed_url):
2015 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2016 return None
wychen3c1c1722016-08-04 11:46:36 -07002017 # Rietveld patch: https://domain/<number>/#ps<patchset>
2018 match = re.match(r'/(\d+)/$', parsed_url.path)
2019 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2020 if match and match2:
2021 return _RietveldParsedIssueNumberArgument(
2022 issue=int(match.group(1)),
2023 patchset=int(match2.group(1)),
2024 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002025 # Typical url: https://domain/<issue_number>[/[other]]
2026 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2027 if match:
2028 return _RietveldParsedIssueNumberArgument(
2029 issue=int(match.group(1)),
2030 hostname=parsed_url.netloc)
2031 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2032 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2033 if match:
2034 return _RietveldParsedIssueNumberArgument(
2035 issue=int(match.group(1)),
2036 patchset=int(match.group(2)),
2037 hostname=parsed_url.netloc,
2038 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
2039 return None
2040
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002041 def CMDUploadChange(self, options, args, change):
2042 """Upload the patch to Rietveld."""
2043 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2044 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002045 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2046 if options.emulate_svn_auto_props:
2047 upload_args.append('--emulate_svn_auto_props')
2048
2049 change_desc = None
2050
2051 if options.email is not None:
2052 upload_args.extend(['--email', options.email])
2053
2054 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002055 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002056 upload_args.extend(['--title', options.title])
2057 if options.message:
2058 upload_args.extend(['--message', options.message])
2059 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002060 print('This branch is associated with issue %s. '
2061 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002062 else:
nodirca166002016-06-27 10:59:51 -07002063 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002064 upload_args.extend(['--title', options.title])
2065 message = (options.title or options.message or
2066 CreateDescriptionFromLog(args))
2067 change_desc = ChangeDescription(message)
2068 if options.reviewers or options.tbr_owners:
2069 change_desc.update_reviewers(options.reviewers,
2070 options.tbr_owners,
2071 change)
2072 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002073 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002074
2075 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002076 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002077 return 1
2078
2079 upload_args.extend(['--message', change_desc.description])
2080 if change_desc.get_reviewers():
2081 upload_args.append('--reviewers=%s' % ','.join(
2082 change_desc.get_reviewers()))
2083 if options.send_mail:
2084 if not change_desc.get_reviewers():
2085 DieWithError("Must specify reviewers to send email.")
2086 upload_args.append('--send_mail')
2087
2088 # We check this before applying rietveld.private assuming that in
2089 # rietveld.cc only addresses which we can send private CLs to are listed
2090 # if rietveld.private is set, and so we should ignore rietveld.cc only
2091 # when --private is specified explicitly on the command line.
2092 if options.private:
2093 logging.warn('rietveld.cc is ignored since private flag is specified. '
2094 'You need to review and add them manually if necessary.')
2095 cc = self.GetCCListWithoutDefault()
2096 else:
2097 cc = self.GetCCList()
2098 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2099 if cc:
2100 upload_args.extend(['--cc', cc])
2101
2102 if options.private or settings.GetDefaultPrivateFlag() == "True":
2103 upload_args.append('--private')
2104
2105 upload_args.extend(['--git_similarity', str(options.similarity)])
2106 if not options.find_copies:
2107 upload_args.extend(['--git_no_find_copies'])
2108
2109 # Include the upstream repo's URL in the change -- this is useful for
2110 # projects that have their source spread across multiple repos.
2111 remote_url = self.GetGitBaseUrlFromConfig()
2112 if not remote_url:
2113 if settings.GetIsGitSvn():
2114 remote_url = self.GetGitSvnRemoteUrl()
2115 else:
2116 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2117 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2118 self.GetUpstreamBranch().split('/')[-1])
2119 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002120 remote, remote_branch = self.GetRemoteBranch()
2121 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2122 settings.GetPendingRefPrefix())
2123 if target_ref:
2124 upload_args.extend(['--target_ref', target_ref])
2125
2126 # Look for dependent patchsets. See crbug.com/480453 for more details.
2127 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2128 upstream_branch = ShortBranchName(upstream_branch)
2129 if remote is '.':
2130 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002131 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002132 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002133 print()
2134 print('Skipping dependency patchset upload because git config '
2135 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2136 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002137 else:
2138 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002139 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002140 auth_config=auth_config)
2141 branch_cl_issue_url = branch_cl.GetIssueURL()
2142 branch_cl_issue = branch_cl.GetIssue()
2143 branch_cl_patchset = branch_cl.GetPatchset()
2144 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2145 upload_args.extend(
2146 ['--depends_on_patchset', '%s:%s' % (
2147 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002148 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 '\n'
2150 'The current branch (%s) is tracking a local branch (%s) with '
2151 'an associated CL.\n'
2152 'Adding %s/#ps%s as a dependency patchset.\n'
2153 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2154 branch_cl_patchset))
2155
2156 project = settings.GetProject()
2157 if project:
2158 upload_args.extend(['--project', project])
2159
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002160 try:
2161 upload_args = ['upload'] + upload_args + args
2162 logging.info('upload.RealMain(%s)', upload_args)
2163 issue, patchset = upload.RealMain(upload_args)
2164 issue = int(issue)
2165 patchset = int(patchset)
2166 except KeyboardInterrupt:
2167 sys.exit(1)
2168 except:
2169 # If we got an exception after the user typed a description for their
2170 # change, back up the description before re-raising.
2171 if change_desc:
2172 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2173 print('\nGot exception while uploading -- saving description to %s\n' %
2174 backup_path)
2175 backup_file = open(backup_path, 'w')
2176 backup_file.write(change_desc.description)
2177 backup_file.close()
2178 raise
2179
2180 if not self.GetIssue():
2181 self.SetIssue(issue)
2182 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 return 0
2184
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002185
2186class _GerritChangelistImpl(_ChangelistCodereviewBase):
2187 def __init__(self, changelist, auth_config=None):
2188 # auth_config is Rietveld thing, kept here to preserve interface only.
2189 super(_GerritChangelistImpl, self).__init__(changelist)
2190 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002191 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002192 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002193 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002194
2195 def _GetGerritHost(self):
2196 # Lazy load of configs.
2197 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002198 if self._gerrit_host and '.' not in self._gerrit_host:
2199 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2200 # This happens for internal stuff http://crbug.com/614312.
2201 parsed = urlparse.urlparse(self.GetRemoteUrl())
2202 if parsed.scheme == 'sso':
2203 print('WARNING: using non https URLs for remote is likely broken\n'
2204 ' Your current remote is: %s' % self.GetRemoteUrl())
2205 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2206 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002207 return self._gerrit_host
2208
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002209 def _GetGitHost(self):
2210 """Returns git host to be used when uploading change to Gerrit."""
2211 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2212
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002213 def GetCodereviewServer(self):
2214 if not self._gerrit_server:
2215 # If we're on a branch then get the server potentially associated
2216 # with that branch.
2217 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002218 self._gerrit_server = self._GitGetBranchConfigValue(
2219 self.CodereviewServerConfigKey())
2220 if self._gerrit_server:
2221 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002222 if not self._gerrit_server:
2223 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2224 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002225 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002226 parts[0] = parts[0] + '-review'
2227 self._gerrit_host = '.'.join(parts)
2228 self._gerrit_server = 'https://%s' % self._gerrit_host
2229 return self._gerrit_server
2230
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002231 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002232 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002233 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002234
tandrii5d48c322016-08-18 16:19:37 -07002235 @classmethod
2236 def PatchsetConfigKey(cls):
2237 return 'gerritpatchset'
2238
2239 @classmethod
2240 def CodereviewServerConfigKey(cls):
2241 return 'gerritserver'
2242
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002243 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002244 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002245 if settings.GetGerritSkipEnsureAuthenticated():
2246 # For projects with unusual authentication schemes.
2247 # See http://crbug.com/603378.
2248 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002249 # Lazy-loader to identify Gerrit and Git hosts.
2250 if gerrit_util.GceAuthenticator.is_gce():
2251 return
2252 self.GetCodereviewServer()
2253 git_host = self._GetGitHost()
2254 assert self._gerrit_server and self._gerrit_host
2255 cookie_auth = gerrit_util.CookiesAuthenticator()
2256
2257 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2258 git_auth = cookie_auth.get_auth_header(git_host)
2259 if gerrit_auth and git_auth:
2260 if gerrit_auth == git_auth:
2261 return
2262 print((
2263 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2264 ' Check your %s or %s file for credentials of hosts:\n'
2265 ' %s\n'
2266 ' %s\n'
2267 ' %s') %
2268 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2269 git_host, self._gerrit_host,
2270 cookie_auth.get_new_password_message(git_host)))
2271 if not force:
2272 ask_for_data('If you know what you are doing, press Enter to continue, '
2273 'Ctrl+C to abort.')
2274 return
2275 else:
2276 missing = (
2277 [] if gerrit_auth else [self._gerrit_host] +
2278 [] if git_auth else [git_host])
2279 DieWithError('Credentials for the following hosts are required:\n'
2280 ' %s\n'
2281 'These are read from %s (or legacy %s)\n'
2282 '%s' % (
2283 '\n '.join(missing),
2284 cookie_auth.get_gitcookies_path(),
2285 cookie_auth.get_netrc_path(),
2286 cookie_auth.get_new_password_message(git_host)))
2287
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002288 def _PostUnsetIssueProperties(self):
2289 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002290 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002291
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002292 def GetRieveldObjForPresubmit(self):
2293 class ThisIsNotRietveldIssue(object):
2294 def __nonzero__(self):
2295 # This is a hack to make presubmit_support think that rietveld is not
2296 # defined, yet still ensure that calls directly result in a decent
2297 # exception message below.
2298 return False
2299
2300 def __getattr__(self, attr):
2301 print(
2302 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2303 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2304 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2305 'or use Rietveld for codereview.\n'
2306 'See also http://crbug.com/579160.' % attr)
2307 raise NotImplementedError()
2308 return ThisIsNotRietveldIssue()
2309
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002310 def GetGerritObjForPresubmit(self):
2311 return presubmit_support.GerritAccessor(self._GetGerritHost())
2312
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002313 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002314 """Apply a rough heuristic to give a simple summary of an issue's review
2315 or CQ status, assuming adherence to a common workflow.
2316
2317 Returns None if no issue for this branch, or one of the following keywords:
2318 * 'error' - error from review tool (including deleted issues)
2319 * 'unsent' - no reviewers added
2320 * 'waiting' - waiting for review
2321 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002322 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2323 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002324 * 'commit' - in the commit queue
2325 * 'closed' - abandoned
2326 """
2327 if not self.GetIssue():
2328 return None
2329
2330 try:
2331 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002332 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002333 return 'error'
2334
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002335 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002336 return 'closed'
2337
2338 cq_label = data['labels'].get('Commit-Queue', {})
2339 if cq_label:
2340 # Vote value is a stringified integer, which we expect from 0 to 2.
2341 vote_value = cq_label.get('value', '0')
2342 vote_text = cq_label.get('values', {}).get(vote_value, '')
2343 if vote_text.lower() == 'commit':
2344 return 'commit'
2345
2346 lgtm_label = data['labels'].get('Code-Review', {})
2347 if lgtm_label:
2348 if 'rejected' in lgtm_label:
2349 return 'not lgtm'
2350 if 'approved' in lgtm_label:
2351 return 'lgtm'
2352
2353 if not data.get('reviewers', {}).get('REVIEWER', []):
2354 return 'unsent'
2355
2356 messages = data.get('messages', [])
2357 if messages:
2358 owner = data['owner'].get('_account_id')
2359 last_message_author = messages[-1].get('author', {}).get('_account_id')
2360 if owner != last_message_author:
2361 # Some reply from non-owner.
2362 return 'reply'
2363
2364 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002365
2366 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002367 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002368 return data['revisions'][data['current_revision']]['_number']
2369
2370 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002371 data = self._GetChangeDetail(['CURRENT_REVISION'])
2372 current_rev = data['current_revision']
2373 url = data['revisions'][current_rev]['fetch']['http']['url']
2374 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375
dsansomee2d6fd92016-09-08 00:10:47 -07002376 def UpdateDescriptionRemote(self, description, force=False):
2377 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2378 if not force:
2379 ask_for_data(
2380 'The description cannot be modified while the issue has a pending '
2381 'unpublished edit. Either publish the edit in the Gerrit web UI '
2382 'or delete it.\n\n'
2383 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2384
2385 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2386 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002387 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2388 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002389
2390 def CloseIssue(self):
2391 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2392
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002393 def GetApprovingReviewers(self):
2394 """Returns a list of reviewers approving the change.
2395
2396 Note: not necessarily committers.
2397 """
2398 raise NotImplementedError()
2399
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002400 def SubmitIssue(self, wait_for_merge=True):
2401 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2402 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002404 def _GetChangeDetail(self, options=None, issue=None):
2405 options = options or []
2406 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002407 assert issue, 'issue is required to query Gerrit'
2408 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002409 options)
tandriic2405f52016-10-10 08:13:15 -07002410 if not data:
2411 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2412 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002413
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002414 def CMDLand(self, force, bypass_hooks, verbose):
2415 if git_common.is_dirty_git_tree('land'):
2416 return 1
tandriid60367b2016-06-22 05:25:12 -07002417 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2418 if u'Commit-Queue' in detail.get('labels', {}):
2419 if not force:
2420 ask_for_data('\nIt seems this repository has a Commit Queue, '
2421 'which can test and land changes for you. '
2422 'Are you sure you wish to bypass it?\n'
2423 'Press Enter to continue, Ctrl+C to abort.')
2424
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002425 differs = True
tandriic4344b52016-08-29 06:04:54 -07002426 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002427 # Note: git diff outputs nothing if there is no diff.
2428 if not last_upload or RunGit(['diff', last_upload]).strip():
2429 print('WARNING: some changes from local branch haven\'t been uploaded')
2430 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002431 if detail['current_revision'] == last_upload:
2432 differs = False
2433 else:
2434 print('WARNING: local branch contents differ from latest uploaded '
2435 'patchset')
2436 if differs:
2437 if not force:
2438 ask_for_data(
2439 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2440 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2441 elif not bypass_hooks:
2442 hook_results = self.RunHook(
2443 committing=True,
2444 may_prompt=not force,
2445 verbose=verbose,
2446 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2447 if not hook_results.should_continue():
2448 return 1
2449
2450 self.SubmitIssue(wait_for_merge=True)
2451 print('Issue %s has been submitted.' % self.GetIssueURL())
2452 return 0
2453
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002454 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2455 directory):
2456 assert not reject
2457 assert not nocommit
2458 assert not directory
2459 assert parsed_issue_arg.valid
2460
2461 self._changelist.issue = parsed_issue_arg.issue
2462
2463 if parsed_issue_arg.hostname:
2464 self._gerrit_host = parsed_issue_arg.hostname
2465 self._gerrit_server = 'https://%s' % self._gerrit_host
2466
tandriic2405f52016-10-10 08:13:15 -07002467 try:
2468 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2469 except GerritIssueNotExists as e:
2470 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002471
2472 if not parsed_issue_arg.patchset:
2473 # Use current revision by default.
2474 revision_info = detail['revisions'][detail['current_revision']]
2475 patchset = int(revision_info['_number'])
2476 else:
2477 patchset = parsed_issue_arg.patchset
2478 for revision_info in detail['revisions'].itervalues():
2479 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2480 break
2481 else:
2482 DieWithError('Couldn\'t find patchset %i in issue %i' %
2483 (parsed_issue_arg.patchset, self.GetIssue()))
2484
2485 fetch_info = revision_info['fetch']['http']
2486 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2487 RunGit(['cherry-pick', 'FETCH_HEAD'])
2488 self.SetIssue(self.GetIssue())
2489 self.SetPatchset(patchset)
2490 print('Committed patch for issue %i pathset %i locally' %
2491 (self.GetIssue(), self.GetPatchset()))
2492 return 0
2493
2494 @staticmethod
2495 def ParseIssueURL(parsed_url):
2496 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2497 return None
2498 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2499 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2500 # Short urls like https://domain/<issue_number> can be used, but don't allow
2501 # specifying the patchset (you'd 404), but we allow that here.
2502 if parsed_url.path == '/':
2503 part = parsed_url.fragment
2504 else:
2505 part = parsed_url.path
2506 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2507 if match:
2508 return _ParsedIssueNumberArgument(
2509 issue=int(match.group(2)),
2510 patchset=int(match.group(4)) if match.group(4) else None,
2511 hostname=parsed_url.netloc)
2512 return None
2513
tandrii16e0b4e2016-06-07 10:34:28 -07002514 def _GerritCommitMsgHookCheck(self, offer_removal):
2515 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2516 if not os.path.exists(hook):
2517 return
2518 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2519 # custom developer made one.
2520 data = gclient_utils.FileRead(hook)
2521 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2522 return
2523 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002524 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002525 'and may interfere with it in subtle ways.\n'
2526 'We recommend you remove the commit-msg hook.')
2527 if offer_removal:
2528 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2529 if reply.lower().startswith('y'):
2530 gclient_utils.rm_file_or_tree(hook)
2531 print('Gerrit commit-msg hook removed.')
2532 else:
2533 print('OK, will keep Gerrit commit-msg hook in place.')
2534
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002535 def CMDUploadChange(self, options, args, change):
2536 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002537 if options.squash and options.no_squash:
2538 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002539
2540 if not options.squash and not options.no_squash:
2541 # Load default for user, repo, squash=true, in this order.
2542 options.squash = settings.GetSquashGerritUploads()
2543 elif options.no_squash:
2544 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002545
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002546 # We assume the remote called "origin" is the one we want.
2547 # It is probably not worthwhile to support different workflows.
2548 gerrit_remote = 'origin'
2549
2550 remote, remote_branch = self.GetRemoteBranch()
2551 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2552 pending_prefix='')
2553
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002554 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002555 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002556 if self.GetIssue():
2557 # Try to get the message from a previous upload.
2558 message = self.GetDescription()
2559 if not message:
2560 DieWithError(
2561 'failed to fetch description from current Gerrit issue %d\n'
2562 '%s' % (self.GetIssue(), self.GetIssueURL()))
2563 change_id = self._GetChangeDetail()['change_id']
2564 while True:
2565 footer_change_ids = git_footers.get_footer_change_id(message)
2566 if footer_change_ids == [change_id]:
2567 break
2568 if not footer_change_ids:
2569 message = git_footers.add_footer_change_id(message, change_id)
2570 print('WARNING: appended missing Change-Id to issue description')
2571 continue
2572 # There is already a valid footer but with different or several ids.
2573 # Doing this automatically is non-trivial as we don't want to lose
2574 # existing other footers, yet we want to append just 1 desired
2575 # Change-Id. Thus, just create a new footer, but let user verify the
2576 # new description.
2577 message = '%s\n\nChange-Id: %s' % (message, change_id)
2578 print(
2579 'WARNING: issue %s has Change-Id footer(s):\n'
2580 ' %s\n'
2581 'but issue has Change-Id %s, according to Gerrit.\n'
2582 'Please, check the proposed correction to the description, '
2583 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2584 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2585 change_id))
2586 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2587 if not options.force:
2588 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002589 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002590 message = change_desc.description
2591 if not message:
2592 DieWithError("Description is empty. Aborting...")
2593 # Continue the while loop.
2594 # Sanity check of this code - we should end up with proper message
2595 # footer.
2596 assert [change_id] == git_footers.get_footer_change_id(message)
2597 change_desc = ChangeDescription(message)
2598 else:
2599 change_desc = ChangeDescription(
2600 options.message or CreateDescriptionFromLog(args))
2601 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002602 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002603 if not change_desc.description:
2604 DieWithError("Description is empty. Aborting...")
2605 message = change_desc.description
2606 change_ids = git_footers.get_footer_change_id(message)
2607 if len(change_ids) > 1:
2608 DieWithError('too many Change-Id footers, at most 1 allowed.')
2609 if not change_ids:
2610 # Generate the Change-Id automatically.
2611 message = git_footers.add_footer_change_id(
2612 message, GenerateGerritChangeId(message))
2613 change_desc.set_description(message)
2614 change_ids = git_footers.get_footer_change_id(message)
2615 assert len(change_ids) == 1
2616 change_id = change_ids[0]
2617
2618 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2619 if remote is '.':
2620 # If our upstream branch is local, we base our squashed commit on its
2621 # squashed version.
2622 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2623 # Check the squashed hash of the parent.
2624 parent = RunGit(['config',
2625 'branch.%s.gerritsquashhash' % upstream_branch_name],
2626 error_ok=True).strip()
2627 # Verify that the upstream branch has been uploaded too, otherwise
2628 # Gerrit will create additional CLs when uploading.
2629 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2630 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002631 DieWithError(
2632 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002633 'Note: maybe you\'ve uploaded it with --no-squash. '
2634 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002635 ' git cl upload --squash\n' % upstream_branch_name)
2636 else:
2637 parent = self.GetCommonAncestorWithUpstream()
2638
2639 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2640 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2641 '-m', message]).strip()
2642 else:
2643 change_desc = ChangeDescription(
2644 options.message or CreateDescriptionFromLog(args))
2645 if not change_desc.description:
2646 DieWithError("Description is empty. Aborting...")
2647
2648 if not git_footers.get_footer_change_id(change_desc.description):
2649 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002650 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2651 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002652 ref_to_push = 'HEAD'
2653 parent = '%s/%s' % (gerrit_remote, branch)
2654 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2655
2656 assert change_desc
2657 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2658 ref_to_push)]).splitlines()
2659 if len(commits) > 1:
2660 print('WARNING: This will upload %d commits. Run the following command '
2661 'to see which commits will be uploaded: ' % len(commits))
2662 print('git log %s..%s' % (parent, ref_to_push))
2663 print('You can also use `git squash-branch` to squash these into a '
2664 'single commit.')
2665 ask_for_data('About to upload; enter to confirm.')
2666
2667 if options.reviewers or options.tbr_owners:
2668 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2669 change)
2670
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002671 # Extra options that can be specified at push time. Doc:
2672 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2673 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002674 if change_desc.get_reviewers(tbr_only=True):
2675 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2676 refspec_opts.append('l=Code-Review+1')
2677
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002678 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002679 if not re.match(r'^[\w ]+$', options.title):
2680 options.title = re.sub(r'[^\w ]', '', options.title)
2681 print('WARNING: Patchset title may only contain alphanumeric chars '
2682 'and spaces. Cleaned up title:\n%s' % options.title)
2683 if not options.force:
2684 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002685 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2686 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002687 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2688
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002689 if options.send_mail:
2690 if not change_desc.get_reviewers():
2691 DieWithError('Must specify reviewers to send email.')
2692 refspec_opts.append('notify=ALL')
2693 else:
2694 refspec_opts.append('notify=NONE')
2695
tandrii99a72f22016-08-17 14:33:24 -07002696 reviewers = change_desc.get_reviewers()
2697 if reviewers:
2698 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002699
agablec6787972016-09-09 16:13:34 -07002700 if options.private:
2701 refspec_opts.append('draft')
2702
rmistry9eadede2016-09-19 11:22:43 -07002703 if options.topic:
2704 # Documentation on Gerrit topics is here:
2705 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2706 refspec_opts.append('topic=%s' % options.topic)
2707
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002708 refspec_suffix = ''
2709 if refspec_opts:
2710 refspec_suffix = '%' + ','.join(refspec_opts)
2711 assert ' ' not in refspec_suffix, (
2712 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002713 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002714
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002716 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002717 print_stdout=True,
2718 # Flush after every line: useful for seeing progress when running as
2719 # recipe.
2720 filter_fn=lambda _: sys.stdout.flush())
2721
2722 if options.squash:
2723 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2724 change_numbers = [m.group(1)
2725 for m in map(regex.match, push_stdout.splitlines())
2726 if m]
2727 if len(change_numbers) != 1:
2728 DieWithError(
2729 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2730 'Change-Id: %s') % (len(change_numbers), change_id))
2731 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002732 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002733
2734 # Add cc's from the CC_LIST and --cc flag (if any).
2735 cc = self.GetCCList().split(',')
2736 if options.cc:
2737 cc.extend(options.cc)
2738 cc = filter(None, [email.strip() for email in cc])
2739 if cc:
2740 gerrit_util.AddReviewers(
2741 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2742
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 return 0
2744
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002745 def _AddChangeIdToCommitMessage(self, options, args):
2746 """Re-commits using the current message, assumes the commit hook is in
2747 place.
2748 """
2749 log_desc = options.message or CreateDescriptionFromLog(args)
2750 git_command = ['commit', '--amend', '-m', log_desc]
2751 RunGit(git_command)
2752 new_log_desc = CreateDescriptionFromLog(args)
2753 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002754 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002755 return new_log_desc
2756 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002757 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002758
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002759 def SetCQState(self, new_state):
2760 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002761 vote_map = {
2762 _CQState.NONE: 0,
2763 _CQState.DRY_RUN: 1,
2764 _CQState.COMMIT : 2,
2765 }
2766 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2767 labels={'Commit-Queue': vote_map[new_state]})
2768
tandriie113dfd2016-10-11 10:20:12 -07002769 def CannotTriggerTryJobReason(self):
2770 # TODO(tandrii): implement for Gerrit.
2771 raise NotImplementedError()
2772
tandriide281ae2016-10-12 06:02:30 -07002773 def GetIssueOwner(self):
2774 # TODO(tandrii): implement for Gerrit.
2775 raise NotImplementedError()
2776
2777 def GetIssueProject(self):
2778 # TODO(tandrii): implement for Gerrit.
2779 raise NotImplementedError()
2780
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002781
2782_CODEREVIEW_IMPLEMENTATIONS = {
2783 'rietveld': _RietveldChangelistImpl,
2784 'gerrit': _GerritChangelistImpl,
2785}
2786
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002787
iannuccie53c9352016-08-17 14:40:40 -07002788def _add_codereview_issue_select_options(parser, extra=""):
2789 _add_codereview_select_options(parser)
2790
2791 text = ('Operate on this issue number instead of the current branch\'s '
2792 'implicit issue.')
2793 if extra:
2794 text += ' '+extra
2795 parser.add_option('-i', '--issue', type=int, help=text)
2796
2797
2798def _process_codereview_issue_select_options(parser, options):
2799 _process_codereview_select_options(parser, options)
2800 if options.issue is not None and not options.forced_codereview:
2801 parser.error('--issue must be specified with either --rietveld or --gerrit')
2802
2803
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002804def _add_codereview_select_options(parser):
2805 """Appends --gerrit and --rietveld options to force specific codereview."""
2806 parser.codereview_group = optparse.OptionGroup(
2807 parser, 'EXPERIMENTAL! Codereview override options')
2808 parser.add_option_group(parser.codereview_group)
2809 parser.codereview_group.add_option(
2810 '--gerrit', action='store_true',
2811 help='Force the use of Gerrit for codereview')
2812 parser.codereview_group.add_option(
2813 '--rietveld', action='store_true',
2814 help='Force the use of Rietveld for codereview')
2815
2816
2817def _process_codereview_select_options(parser, options):
2818 if options.gerrit and options.rietveld:
2819 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2820 options.forced_codereview = None
2821 if options.gerrit:
2822 options.forced_codereview = 'gerrit'
2823 elif options.rietveld:
2824 options.forced_codereview = 'rietveld'
2825
2826
tandriif9aefb72016-07-01 09:06:51 -07002827def _get_bug_line_values(default_project, bugs):
2828 """Given default_project and comma separated list of bugs, yields bug line
2829 values.
2830
2831 Each bug can be either:
2832 * a number, which is combined with default_project
2833 * string, which is left as is.
2834
2835 This function may produce more than one line, because bugdroid expects one
2836 project per line.
2837
2838 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2839 ['v8:123', 'chromium:789']
2840 """
2841 default_bugs = []
2842 others = []
2843 for bug in bugs.split(','):
2844 bug = bug.strip()
2845 if bug:
2846 try:
2847 default_bugs.append(int(bug))
2848 except ValueError:
2849 others.append(bug)
2850
2851 if default_bugs:
2852 default_bugs = ','.join(map(str, default_bugs))
2853 if default_project:
2854 yield '%s:%s' % (default_project, default_bugs)
2855 else:
2856 yield default_bugs
2857 for other in sorted(others):
2858 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2859 yield other
2860
2861
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002862class ChangeDescription(object):
2863 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002864 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002865 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002866
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002867 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002868 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002869
agable@chromium.org42c20792013-09-12 17:34:49 +00002870 @property # www.logilab.org/ticket/89786
2871 def description(self): # pylint: disable=E0202
2872 return '\n'.join(self._description_lines)
2873
2874 def set_description(self, desc):
2875 if isinstance(desc, basestring):
2876 lines = desc.splitlines()
2877 else:
2878 lines = [line.rstrip() for line in desc]
2879 while lines and not lines[0]:
2880 lines.pop(0)
2881 while lines and not lines[-1]:
2882 lines.pop(-1)
2883 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002884
piman@chromium.org336f9122014-09-04 02:16:55 +00002885 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002886 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002887 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002888 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002889 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002890 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002891
agable@chromium.org42c20792013-09-12 17:34:49 +00002892 # Get the set of R= and TBR= lines and remove them from the desciption.
2893 regexp = re.compile(self.R_LINE)
2894 matches = [regexp.match(line) for line in self._description_lines]
2895 new_desc = [l for i, l in enumerate(self._description_lines)
2896 if not matches[i]]
2897 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002898
agable@chromium.org42c20792013-09-12 17:34:49 +00002899 # Construct new unified R= and TBR= lines.
2900 r_names = []
2901 tbr_names = []
2902 for match in matches:
2903 if not match:
2904 continue
2905 people = cleanup_list([match.group(2).strip()])
2906 if match.group(1) == 'TBR':
2907 tbr_names.extend(people)
2908 else:
2909 r_names.extend(people)
2910 for name in r_names:
2911 if name not in reviewers:
2912 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002913 if add_owners_tbr:
2914 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002915 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002916 all_reviewers = set(tbr_names + reviewers)
2917 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2918 all_reviewers)
2919 tbr_names.extend(owners_db.reviewers_for(missing_files,
2920 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002921 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2922 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2923
2924 # Put the new lines in the description where the old first R= line was.
2925 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2926 if 0 <= line_loc < len(self._description_lines):
2927 if new_tbr_line:
2928 self._description_lines.insert(line_loc, new_tbr_line)
2929 if new_r_line:
2930 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002931 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002932 if new_r_line:
2933 self.append_footer(new_r_line)
2934 if new_tbr_line:
2935 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002936
tandriif9aefb72016-07-01 09:06:51 -07002937 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002938 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002939 self.set_description([
2940 '# Enter a description of the change.',
2941 '# This will be displayed on the codereview site.',
2942 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002943 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002944 '--------------------',
2945 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002946
agable@chromium.org42c20792013-09-12 17:34:49 +00002947 regexp = re.compile(self.BUG_LINE)
2948 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002949 prefix = settings.GetBugPrefix()
2950 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2951 for value in values:
2952 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2953 self.append_footer('BUG=%s' % value)
2954
agable@chromium.org42c20792013-09-12 17:34:49 +00002955 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002956 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002957 if not content:
2958 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002959 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002960
2961 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002962 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2963 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002964 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002965 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002966
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002967 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002968 """Adds a footer line to the description.
2969
2970 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2971 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2972 that Gerrit footers are always at the end.
2973 """
2974 parsed_footer_line = git_footers.parse_footer(line)
2975 if parsed_footer_line:
2976 # Line is a gerrit footer in the form: Footer-Key: any value.
2977 # Thus, must be appended observing Gerrit footer rules.
2978 self.set_description(
2979 git_footers.add_footer(self.description,
2980 key=parsed_footer_line[0],
2981 value=parsed_footer_line[1]))
2982 return
2983
2984 if not self._description_lines:
2985 self._description_lines.append(line)
2986 return
2987
2988 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2989 if gerrit_footers:
2990 # git_footers.split_footers ensures that there is an empty line before
2991 # actual (gerrit) footers, if any. We have to keep it that way.
2992 assert top_lines and top_lines[-1] == ''
2993 top_lines, separator = top_lines[:-1], top_lines[-1:]
2994 else:
2995 separator = [] # No need for separator if there are no gerrit_footers.
2996
2997 prev_line = top_lines[-1] if top_lines else ''
2998 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2999 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3000 top_lines.append('')
3001 top_lines.append(line)
3002 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003003
tandrii99a72f22016-08-17 14:33:24 -07003004 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003007 reviewers = [match.group(2).strip()
3008 for match in matches
3009 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003010 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003011
3012
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003013def get_approving_reviewers(props):
3014 """Retrieves the reviewers that approved a CL from the issue properties with
3015 messages.
3016
3017 Note that the list may contain reviewers that are not committer, thus are not
3018 considered by the CQ.
3019 """
3020 return sorted(
3021 set(
3022 message['sender']
3023 for message in props['messages']
3024 if message['approval'] and message['sender'] in props['reviewers']
3025 )
3026 )
3027
3028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003029def FindCodereviewSettingsFile(filename='codereview.settings'):
3030 """Finds the given file starting in the cwd and going up.
3031
3032 Only looks up to the top of the repository unless an
3033 'inherit-review-settings-ok' file exists in the root of the repository.
3034 """
3035 inherit_ok_file = 'inherit-review-settings-ok'
3036 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003037 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003038 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3039 root = '/'
3040 while True:
3041 if filename in os.listdir(cwd):
3042 if os.path.isfile(os.path.join(cwd, filename)):
3043 return open(os.path.join(cwd, filename))
3044 if cwd == root:
3045 break
3046 cwd = os.path.dirname(cwd)
3047
3048
3049def LoadCodereviewSettingsFromFile(fileobj):
3050 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003051 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053 def SetProperty(name, setting, unset_error_ok=False):
3054 fullname = 'rietveld.' + name
3055 if setting in keyvals:
3056 RunGit(['config', fullname, keyvals[setting]])
3057 else:
3058 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3059
3060 SetProperty('server', 'CODE_REVIEW_SERVER')
3061 # Only server setting is required. Other settings can be absent.
3062 # In that case, we ignore errors raised during option deletion attempt.
3063 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003064 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3066 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003067 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003068 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003069 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3070 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003071 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003072 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003073 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003074 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003075 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3076 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003077
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003078 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003079 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003080
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003081 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003082 RunGit(['config', 'gerrit.squash-uploads',
3083 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003084
tandrii@chromium.org28253532016-04-14 13:46:56 +00003085 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003086 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003087 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3090 #should be of the form
3091 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3092 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3093 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3094 keyvals['ORIGIN_URL_CONFIG']])
3095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003096
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003097def urlretrieve(source, destination):
3098 """urllib is broken for SSL connections via a proxy therefore we
3099 can't use urllib.urlretrieve()."""
3100 with open(destination, 'w') as f:
3101 f.write(urllib2.urlopen(source).read())
3102
3103
ukai@chromium.org712d6102013-11-27 00:52:58 +00003104def hasSheBang(fname):
3105 """Checks fname is a #! script."""
3106 with open(fname) as f:
3107 return f.read(2).startswith('#!')
3108
3109
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003110# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3111def DownloadHooks(*args, **kwargs):
3112 pass
3113
3114
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003115def DownloadGerritHook(force):
3116 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003117
3118 Args:
3119 force: True to update hooks. False to install hooks if not present.
3120 """
3121 if not settings.GetIsGerrit():
3122 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003123 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003124 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3125 if not os.access(dst, os.X_OK):
3126 if os.path.exists(dst):
3127 if not force:
3128 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003129 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003130 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003131 if not hasSheBang(dst):
3132 DieWithError('Not a script: %s\n'
3133 'You need to download from\n%s\n'
3134 'into .git/hooks/commit-msg and '
3135 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003136 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3137 except Exception:
3138 if os.path.exists(dst):
3139 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003140 DieWithError('\nFailed to download hooks.\n'
3141 'You need to download from\n%s\n'
3142 'into .git/hooks/commit-msg and '
3143 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003144
3145
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003146
3147def GetRietveldCodereviewSettingsInteractively():
3148 """Prompt the user for settings."""
3149 server = settings.GetDefaultServerUrl(error_ok=True)
3150 prompt = 'Rietveld server (host[:port])'
3151 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3152 newserver = ask_for_data(prompt + ':')
3153 if not server and not newserver:
3154 newserver = DEFAULT_SERVER
3155 if newserver:
3156 newserver = gclient_utils.UpgradeToHttps(newserver)
3157 if newserver != server:
3158 RunGit(['config', 'rietveld.server', newserver])
3159
3160 def SetProperty(initial, caption, name, is_url):
3161 prompt = caption
3162 if initial:
3163 prompt += ' ("x" to clear) [%s]' % initial
3164 new_val = ask_for_data(prompt + ':')
3165 if new_val == 'x':
3166 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3167 elif new_val:
3168 if is_url:
3169 new_val = gclient_utils.UpgradeToHttps(new_val)
3170 if new_val != initial:
3171 RunGit(['config', 'rietveld.' + name, new_val])
3172
3173 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3174 SetProperty(settings.GetDefaultPrivateFlag(),
3175 'Private flag (rietveld only)', 'private', False)
3176 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3177 'tree-status-url', False)
3178 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3179 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3180 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3181 'run-post-upload-hook', False)
3182
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003183@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003184def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003185 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186
tandrii5d0a0422016-09-14 06:24:35 -07003187 print('WARNING: git cl config works for Rietveld only')
3188 # TODO(tandrii): remove this once we switch to Gerrit.
3189 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003190 parser.add_option('--activate-update', action='store_true',
3191 help='activate auto-updating [rietveld] section in '
3192 '.git/config')
3193 parser.add_option('--deactivate-update', action='store_true',
3194 help='deactivate auto-updating [rietveld] section in '
3195 '.git/config')
3196 options, args = parser.parse_args(args)
3197
3198 if options.deactivate_update:
3199 RunGit(['config', 'rietveld.autoupdate', 'false'])
3200 return
3201
3202 if options.activate_update:
3203 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3204 return
3205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003207 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 return 0
3209
3210 url = args[0]
3211 if not url.endswith('codereview.settings'):
3212 url = os.path.join(url, 'codereview.settings')
3213
3214 # Load code review settings and download hooks (if available).
3215 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3216 return 0
3217
3218
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003219def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003220 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003221 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3222 branch = ShortBranchName(branchref)
3223 _, args = parser.parse_args(args)
3224 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003225 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003226 return RunGit(['config', 'branch.%s.base-url' % branch],
3227 error_ok=False).strip()
3228 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003229 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003230 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3231 error_ok=False).strip()
3232
3233
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003234def color_for_status(status):
3235 """Maps a Changelist status to color, for CMDstatus and other tools."""
3236 return {
3237 'unsent': Fore.RED,
3238 'waiting': Fore.BLUE,
3239 'reply': Fore.YELLOW,
3240 'lgtm': Fore.GREEN,
3241 'commit': Fore.MAGENTA,
3242 'closed': Fore.CYAN,
3243 'error': Fore.WHITE,
3244 }.get(status, Fore.WHITE)
3245
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003246
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003247def get_cl_statuses(changes, fine_grained, max_processes=None):
3248 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003249
3250 If fine_grained is true, this will fetch CL statuses from the server.
3251 Otherwise, simply indicate if there's a matching url for the given branches.
3252
3253 If max_processes is specified, it is used as the maximum number of processes
3254 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3255 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003256
3257 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003258 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003259 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003260 upload.verbosity = 0
3261
3262 if fine_grained:
3263 # Process one branch synchronously to work through authentication, then
3264 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003265 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003266 def fetch(cl):
3267 try:
3268 return (cl, cl.GetStatus())
3269 except:
3270 # See http://crbug.com/629863.
3271 logging.exception('failed to fetch status for %s:', cl)
3272 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003273 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003274
tandriiea9514a2016-08-17 12:32:37 -07003275 changes_to_fetch = changes[1:]
3276 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003277 # Exit early if there was only one branch to fetch.
3278 return
3279
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003280 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003281 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003282 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003283 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003284
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003285 fetched_cls = set()
3286 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003287 while True:
3288 try:
3289 row = it.next(timeout=5)
3290 except multiprocessing.TimeoutError:
3291 break
3292
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003293 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003294 yield row
3295
3296 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003297 for cl in set(changes_to_fetch) - fetched_cls:
3298 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003299
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003300 else:
3301 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003302 for cl in changes:
3303 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003304
rmistry@google.com2dd99862015-06-22 12:22:18 +00003305
3306def upload_branch_deps(cl, args):
3307 """Uploads CLs of local branches that are dependents of the current branch.
3308
3309 If the local branch dependency tree looks like:
3310 test1 -> test2.1 -> test3.1
3311 -> test3.2
3312 -> test2.2 -> test3.3
3313
3314 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3315 run on the dependent branches in this order:
3316 test2.1, test3.1, test3.2, test2.2, test3.3
3317
3318 Note: This function does not rebase your local dependent branches. Use it when
3319 you make a change to the parent branch that will not conflict with its
3320 dependent branches, and you would like their dependencies updated in
3321 Rietveld.
3322 """
3323 if git_common.is_dirty_git_tree('upload-branch-deps'):
3324 return 1
3325
3326 root_branch = cl.GetBranch()
3327 if root_branch is None:
3328 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3329 'Get on a branch!')
3330 if not cl.GetIssue() or not cl.GetPatchset():
3331 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3332 'patchset dependencies without an uploaded CL.')
3333
3334 branches = RunGit(['for-each-ref',
3335 '--format=%(refname:short) %(upstream:short)',
3336 'refs/heads'])
3337 if not branches:
3338 print('No local branches found.')
3339 return 0
3340
3341 # Create a dictionary of all local branches to the branches that are dependent
3342 # on it.
3343 tracked_to_dependents = collections.defaultdict(list)
3344 for b in branches.splitlines():
3345 tokens = b.split()
3346 if len(tokens) == 2:
3347 branch_name, tracked = tokens
3348 tracked_to_dependents[tracked].append(branch_name)
3349
vapiera7fbd5a2016-06-16 09:17:49 -07003350 print()
3351 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003352 dependents = []
3353 def traverse_dependents_preorder(branch, padding=''):
3354 dependents_to_process = tracked_to_dependents.get(branch, [])
3355 padding += ' '
3356 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003357 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003358 dependents.append(dependent)
3359 traverse_dependents_preorder(dependent, padding)
3360 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003361 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003362
3363 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003364 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003365 return 0
3366
vapiera7fbd5a2016-06-16 09:17:49 -07003367 print('This command will checkout all dependent branches and run '
3368 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003369 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3370
andybons@chromium.org962f9462016-02-03 20:00:42 +00003371 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003372 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003373 args.extend(['-t', 'Updated patchset dependency'])
3374
rmistry@google.com2dd99862015-06-22 12:22:18 +00003375 # Record all dependents that failed to upload.
3376 failures = {}
3377 # Go through all dependents, checkout the branch and upload.
3378 try:
3379 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003380 print()
3381 print('--------------------------------------')
3382 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003383 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003384 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003385 try:
3386 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003387 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003388 failures[dependent_branch] = 1
3389 except: # pylint: disable=W0702
3390 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003391 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003392 finally:
3393 # Swap back to the original root branch.
3394 RunGit(['checkout', '-q', root_branch])
3395
vapiera7fbd5a2016-06-16 09:17:49 -07003396 print()
3397 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003398 for dependent_branch in dependents:
3399 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003400 print(' %s : %s' % (dependent_branch, upload_status))
3401 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003402
3403 return 0
3404
3405
kmarshall3bff56b2016-06-06 18:31:47 -07003406def CMDarchive(parser, args):
3407 """Archives and deletes branches associated with closed changelists."""
3408 parser.add_option(
3409 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003410 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003411 parser.add_option(
3412 '-f', '--force', action='store_true',
3413 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003414 parser.add_option(
3415 '-d', '--dry-run', action='store_true',
3416 help='Skip the branch tagging and removal steps.')
3417 parser.add_option(
3418 '-t', '--notags', action='store_true',
3419 help='Do not tag archived branches. '
3420 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003421
3422 auth.add_auth_options(parser)
3423 options, args = parser.parse_args(args)
3424 if args:
3425 parser.error('Unsupported args: %s' % ' '.join(args))
3426 auth_config = auth.extract_auth_config_from_options(options)
3427
3428 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3429 if not branches:
3430 return 0
3431
vapiera7fbd5a2016-06-16 09:17:49 -07003432 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003433 changes = [Changelist(branchref=b, auth_config=auth_config)
3434 for b in branches.splitlines()]
3435 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3436 statuses = get_cl_statuses(changes,
3437 fine_grained=True,
3438 max_processes=options.maxjobs)
3439 proposal = [(cl.GetBranch(),
3440 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3441 for cl, status in statuses
3442 if status == 'closed']
3443 proposal.sort()
3444
3445 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003446 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003447 return 0
3448
3449 current_branch = GetCurrentBranch()
3450
vapiera7fbd5a2016-06-16 09:17:49 -07003451 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003452 if options.notags:
3453 for next_item in proposal:
3454 print(' ' + next_item[0])
3455 else:
3456 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3457 for next_item in proposal:
3458 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003459
kmarshall9249e012016-08-23 12:02:16 -07003460 # Quit now on precondition failure or if instructed by the user, either
3461 # via an interactive prompt or by command line flags.
3462 if options.dry_run:
3463 print('\nNo changes were made (dry run).\n')
3464 return 0
3465 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003466 print('You are currently on a branch \'%s\' which is associated with a '
3467 'closed codereview issue, so archive cannot proceed. Please '
3468 'checkout another branch and run this command again.' %
3469 current_branch)
3470 return 1
kmarshall9249e012016-08-23 12:02:16 -07003471 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003472 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3473 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003474 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003475 return 1
3476
3477 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003478 if not options.notags:
3479 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003480 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003481
vapiera7fbd5a2016-06-16 09:17:49 -07003482 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003483
3484 return 0
3485
3486
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003487def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003488 """Show status of changelists.
3489
3490 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003491 - Red not sent for review or broken
3492 - Blue waiting for review
3493 - Yellow waiting for you to reply to review
3494 - Green LGTM'ed
3495 - Magenta in the commit queue
3496 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003497
3498 Also see 'git cl comments'.
3499 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003500 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003501 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003502 parser.add_option('-f', '--fast', action='store_true',
3503 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003504 parser.add_option(
3505 '-j', '--maxjobs', action='store', type=int,
3506 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003507
3508 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003509 _add_codereview_issue_select_options(
3510 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003511 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003512 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003513 if args:
3514 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003515 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003516
iannuccie53c9352016-08-17 14:40:40 -07003517 if options.issue is not None and not options.field:
3518 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003520 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003521 cl = Changelist(auth_config=auth_config, issue=options.issue,
3522 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003524 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525 elif options.field == 'id':
3526 issueid = cl.GetIssue()
3527 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003528 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003529 elif options.field == 'patch':
3530 patchset = cl.GetPatchset()
3531 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003533 elif options.field == 'status':
3534 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003535 elif options.field == 'url':
3536 url = cl.GetIssueURL()
3537 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003538 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003539 return 0
3540
3541 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3542 if not branches:
3543 print('No local branch found.')
3544 return 0
3545
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003546 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003547 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003548 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003550 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003551 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003552 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003553
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003554 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003555 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3556 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3557 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003558 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003559 c, status = output.next()
3560 branch_statuses[c.GetBranch()] = status
3561 status = branch_statuses.pop(branch)
3562 url = cl.GetIssueURL()
3563 if url and (not status or status == 'error'):
3564 # The issue probably doesn't exist anymore.
3565 url += ' (broken)'
3566
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003567 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003568 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003569 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003570 color = ''
3571 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003572 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003573 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003574 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003575 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003576
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003577 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print()
3579 print('Current branch:',)
3580 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003581 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003582 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003583 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003584 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003585 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print('Issue description:')
3587 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588 return 0
3589
3590
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003591def colorize_CMDstatus_doc():
3592 """To be called once in main() to add colors to git cl status help."""
3593 colors = [i for i in dir(Fore) if i[0].isupper()]
3594
3595 def colorize_line(line):
3596 for color in colors:
3597 if color in line.upper():
3598 # Extract whitespaces first and the leading '-'.
3599 indent = len(line) - len(line.lstrip(' ')) + 1
3600 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3601 return line
3602
3603 lines = CMDstatus.__doc__.splitlines()
3604 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3605
3606
phajdan.jre328cf92016-08-22 04:12:17 -07003607def write_json(path, contents):
3608 with open(path, 'w') as f:
3609 json.dump(contents, f)
3610
3611
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003612@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003613def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003614 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003615
3616 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003617 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003618 parser.add_option('-r', '--reverse', action='store_true',
3619 help='Lookup the branch(es) for the specified issues. If '
3620 'no issues are specified, all branches with mapped '
3621 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003622 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003623 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003624 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003625 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003626
dnj@chromium.org406c4402015-03-03 17:22:28 +00003627 if options.reverse:
3628 branches = RunGit(['for-each-ref', 'refs/heads',
3629 '--format=%(refname:short)']).splitlines()
3630
3631 # Reverse issue lookup.
3632 issue_branch_map = {}
3633 for branch in branches:
3634 cl = Changelist(branchref=branch)
3635 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3636 if not args:
3637 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003638 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003639 for issue in args:
3640 if not issue:
3641 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003642 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003643 print('Branch for issue number %s: %s' % (
3644 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003645 if options.json:
3646 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003647 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003648 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003649 if len(args) > 0:
3650 try:
3651 issue = int(args[0])
3652 except ValueError:
3653 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003654 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003655 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003656 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003657 if options.json:
3658 write_json(options.json, {
3659 'issue': cl.GetIssue(),
3660 'issue_url': cl.GetIssueURL(),
3661 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 return 0
3663
3664
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003665def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003666 """Shows or posts review comments for any changelist."""
3667 parser.add_option('-a', '--add-comment', dest='comment',
3668 help='comment to add to an issue')
3669 parser.add_option('-i', dest='issue',
3670 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003671 parser.add_option('-j', '--json-file',
3672 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003673 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003674 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003675 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003676
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003677 issue = None
3678 if options.issue:
3679 try:
3680 issue = int(options.issue)
3681 except ValueError:
3682 DieWithError('A review issue id is expected to be a number')
3683
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003684 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003685
3686 if options.comment:
3687 cl.AddComment(options.comment)
3688 return 0
3689
3690 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003691 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003692 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003693 summary.append({
3694 'date': message['date'],
3695 'lgtm': False,
3696 'message': message['text'],
3697 'not_lgtm': False,
3698 'sender': message['sender'],
3699 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003700 if message['disapproval']:
3701 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003702 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003703 elif message['approval']:
3704 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003705 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003706 elif message['sender'] == data['owner_email']:
3707 color = Fore.MAGENTA
3708 else:
3709 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003710 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003711 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003712 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003713 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003714 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003715 if options.json_file:
3716 with open(options.json_file, 'wb') as f:
3717 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003718 return 0
3719
3720
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003721@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003722def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003723 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003724 parser.add_option('-d', '--display', action='store_true',
3725 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003726 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003727 help='New description to set for this issue (- for stdin, '
3728 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003729 parser.add_option('-f', '--force', action='store_true',
3730 help='Delete any unpublished Gerrit edits for this issue '
3731 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003732
3733 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003734 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003735 options, args = parser.parse_args(args)
3736 _process_codereview_select_options(parser, options)
3737
3738 target_issue = None
3739 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003740 target_issue = ParseIssueNumberArgument(args[0])
3741 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003742 parser.print_help()
3743 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003744
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003745 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003746
martiniss6eda05f2016-06-30 10:18:35 -07003747 kwargs = {
3748 'auth_config': auth_config,
3749 'codereview': options.forced_codereview,
3750 }
3751 if target_issue:
3752 kwargs['issue'] = target_issue.issue
3753 if options.forced_codereview == 'rietveld':
3754 kwargs['rietveld_server'] = target_issue.hostname
3755
3756 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003757
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003758 if not cl.GetIssue():
3759 DieWithError('This branch has no associated changelist.')
3760 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003761
smut@google.com34fb6b12015-07-13 20:03:26 +00003762 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003763 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003764 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003765
3766 if options.new_description:
3767 text = options.new_description
3768 if text == '-':
3769 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003770 elif text == '+':
3771 base_branch = cl.GetCommonAncestorWithUpstream()
3772 change = cl.GetChange(base_branch, None, local_description=True)
3773 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003774
3775 description.set_description(text)
3776 else:
3777 description.prompt()
3778
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003779 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003780 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003781 return 0
3782
3783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784def CreateDescriptionFromLog(args):
3785 """Pulls out the commit log to use as a base for the CL description."""
3786 log_args = []
3787 if len(args) == 1 and not args[0].endswith('.'):
3788 log_args = [args[0] + '..']
3789 elif len(args) == 1 and args[0].endswith('...'):
3790 log_args = [args[0][:-1]]
3791 elif len(args) == 2:
3792 log_args = [args[0] + '..' + args[1]]
3793 else:
3794 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003795 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796
3797
thestig@chromium.org44202a22014-03-11 19:22:18 +00003798def CMDlint(parser, args):
3799 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003800 parser.add_option('--filter', action='append', metavar='-x,+y',
3801 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003802 auth.add_auth_options(parser)
3803 options, args = parser.parse_args(args)
3804 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003805
3806 # Access to a protected member _XX of a client class
3807 # pylint: disable=W0212
3808 try:
3809 import cpplint
3810 import cpplint_chromium
3811 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003812 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003813 return 1
3814
3815 # Change the current working directory before calling lint so that it
3816 # shows the correct base.
3817 previous_cwd = os.getcwd()
3818 os.chdir(settings.GetRoot())
3819 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003820 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003821 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3822 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003823 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003824 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003825 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003826
3827 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003828 command = args + files
3829 if options.filter:
3830 command = ['--filter=' + ','.join(options.filter)] + command
3831 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003832
3833 white_regex = re.compile(settings.GetLintRegex())
3834 black_regex = re.compile(settings.GetLintIgnoreRegex())
3835 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3836 for filename in filenames:
3837 if white_regex.match(filename):
3838 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003839 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003840 else:
3841 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3842 extra_check_functions)
3843 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003844 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003845 finally:
3846 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003847 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003848 if cpplint._cpplint_state.error_count != 0:
3849 return 1
3850 return 0
3851
3852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003854 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003855 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003856 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003857 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003858 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003859 auth.add_auth_options(parser)
3860 options, args = parser.parse_args(args)
3861 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862
sbc@chromium.org71437c02015-04-09 19:29:40 +00003863 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003865 return 1
3866
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003867 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868 if args:
3869 base_branch = args[0]
3870 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003871 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003872 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003873
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003874 cl.RunHook(
3875 committing=not options.upload,
3876 may_prompt=False,
3877 verbose=options.verbose,
3878 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003879 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003880
3881
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003882def GenerateGerritChangeId(message):
3883 """Returns Ixxxxxx...xxx change id.
3884
3885 Works the same way as
3886 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3887 but can be called on demand on all platforms.
3888
3889 The basic idea is to generate git hash of a state of the tree, original commit
3890 message, author/committer info and timestamps.
3891 """
3892 lines = []
3893 tree_hash = RunGitSilent(['write-tree'])
3894 lines.append('tree %s' % tree_hash.strip())
3895 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3896 if code == 0:
3897 lines.append('parent %s' % parent.strip())
3898 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3899 lines.append('author %s' % author.strip())
3900 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3901 lines.append('committer %s' % committer.strip())
3902 lines.append('')
3903 # Note: Gerrit's commit-hook actually cleans message of some lines and
3904 # whitespace. This code is not doing this, but it clearly won't decrease
3905 # entropy.
3906 lines.append(message)
3907 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3908 stdin='\n'.join(lines))
3909 return 'I%s' % change_hash.strip()
3910
3911
wittman@chromium.org455dc922015-01-26 20:15:50 +00003912def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3913 """Computes the remote branch ref to use for the CL.
3914
3915 Args:
3916 remote (str): The git remote for the CL.
3917 remote_branch (str): The git remote branch for the CL.
3918 target_branch (str): The target branch specified by the user.
3919 pending_prefix (str): The pending prefix from the settings.
3920 """
3921 if not (remote and remote_branch):
3922 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003923
wittman@chromium.org455dc922015-01-26 20:15:50 +00003924 if target_branch:
3925 # Cannonicalize branch references to the equivalent local full symbolic
3926 # refs, which are then translated into the remote full symbolic refs
3927 # below.
3928 if '/' not in target_branch:
3929 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3930 else:
3931 prefix_replacements = (
3932 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3933 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3934 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3935 )
3936 match = None
3937 for regex, replacement in prefix_replacements:
3938 match = re.search(regex, target_branch)
3939 if match:
3940 remote_branch = target_branch.replace(match.group(0), replacement)
3941 break
3942 if not match:
3943 # This is a branch path but not one we recognize; use as-is.
3944 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003945 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3946 # Handle the refs that need to land in different refs.
3947 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003948
wittman@chromium.org455dc922015-01-26 20:15:50 +00003949 # Create the true path to the remote branch.
3950 # Does the following translation:
3951 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3952 # * refs/remotes/origin/master -> refs/heads/master
3953 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3954 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3955 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3956 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3957 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3958 'refs/heads/')
3959 elif remote_branch.startswith('refs/remotes/branch-heads'):
3960 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3961 # If a pending prefix exists then replace refs/ with it.
3962 if pending_prefix:
3963 remote_branch = remote_branch.replace('refs/', pending_prefix)
3964 return remote_branch
3965
3966
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003967def cleanup_list(l):
3968 """Fixes a list so that comma separated items are put as individual items.
3969
3970 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3971 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3972 """
3973 items = sum((i.split(',') for i in l), [])
3974 stripped_items = (i.strip() for i in items)
3975 return sorted(filter(None, stripped_items))
3976
3977
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003978@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003979def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003980 """Uploads the current changelist to codereview.
3981
3982 Can skip dependency patchset uploads for a branch by running:
3983 git config branch.branch_name.skip-deps-uploads True
3984 To unset run:
3985 git config --unset branch.branch_name.skip-deps-uploads
3986 Can also set the above globally by using the --global flag.
3987 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003988 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3989 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003990 parser.add_option('--bypass-watchlists', action='store_true',
3991 dest='bypass_watchlists',
3992 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003993 parser.add_option('-f', action='store_true', dest='force',
3994 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003995 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003996 parser.add_option('-b', '--bug',
3997 help='pre-populate the bug number(s) for this issue. '
3998 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003999 parser.add_option('--message-file', dest='message_file',
4000 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004001 parser.add_option('-t', dest='title',
4002 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004003 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004004 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004005 help='reviewer email addresses')
4006 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004007 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004008 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004009 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004010 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004011 parser.add_option('--emulate_svn_auto_props',
4012 '--emulate-svn-auto-props',
4013 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004014 dest="emulate_svn_auto_props",
4015 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004016 parser.add_option('-c', '--use-commit-queue', action='store_true',
4017 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004018 parser.add_option('--private', action='store_true',
4019 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004020 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004021 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004022 metavar='TARGET',
4023 help='Apply CL to remote ref TARGET. ' +
4024 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004025 parser.add_option('--squash', action='store_true',
4026 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004027 parser.add_option('--no-squash', action='store_true',
4028 help='Don\'t squash multiple commits into one ' +
4029 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004030 parser.add_option('--topic', default=None,
4031 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004032 parser.add_option('--email', default=None,
4033 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004034 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4035 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004036 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4037 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004038 help='Send the patchset to do a CQ dry run right after '
4039 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004040 parser.add_option('--dependencies', action='store_true',
4041 help='Uploads CLs of all the local branches that depend on '
4042 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004043
rmistry@google.com2dd99862015-06-22 12:22:18 +00004044 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004045 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004046 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004047 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004048 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004049 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004050 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004051
sbc@chromium.org71437c02015-04-09 19:29:40 +00004052 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004053 return 1
4054
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004055 options.reviewers = cleanup_list(options.reviewers)
4056 options.cc = cleanup_list(options.cc)
4057
tandriib80458a2016-06-23 12:20:07 -07004058 if options.message_file:
4059 if options.message:
4060 parser.error('only one of --message and --message-file allowed.')
4061 options.message = gclient_utils.FileRead(options.message_file)
4062 options.message_file = None
4063
tandrii4d0545a2016-07-06 03:56:49 -07004064 if options.cq_dry_run and options.use_commit_queue:
4065 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4066
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004067 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4068 settings.GetIsGerrit()
4069
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004070 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004071 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004072
4073
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004074def IsSubmoduleMergeCommit(ref):
4075 # When submodules are added to the repo, we expect there to be a single
4076 # non-git-svn merge commit at remote HEAD with a signature comment.
4077 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004078 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004079 return RunGit(cmd) != ''
4080
4081
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004083 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004085 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4086 upstream and closes the issue automatically and atomically.
4087
4088 Otherwise (in case of Rietveld):
4089 Squashes branch into a single commit.
4090 Updates changelog with metadata (e.g. pointer to review).
4091 Pushes/dcommits the code upstream.
4092 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093 """
4094 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4095 help='bypass upload presubmit hook')
4096 parser.add_option('-m', dest='message',
4097 help="override review description")
4098 parser.add_option('-f', action='store_true', dest='force',
4099 help="force yes to questions (don't prompt)")
4100 parser.add_option('-c', dest='contributor',
4101 help="external contributor for patch (appended to " +
4102 "description and used as author for git). Should be " +
4103 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004104 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004105 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004106 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004107 auth_config = auth.extract_auth_config_from_options(options)
4108
4109 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004110
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004111 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4112 if cl.IsGerrit():
4113 if options.message:
4114 # This could be implemented, but it requires sending a new patch to
4115 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4116 # Besides, Gerrit has the ability to change the commit message on submit
4117 # automatically, thus there is no need to support this option (so far?).
4118 parser.error('-m MESSAGE option is not supported for Gerrit.')
4119 if options.contributor:
4120 parser.error(
4121 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4122 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4123 'the contributor\'s "name <email>". If you can\'t upload such a '
4124 'commit for review, contact your repository admin and request'
4125 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004126 if not cl.GetIssue():
4127 DieWithError('You must upload the issue first to Gerrit.\n'
4128 ' If you would rather have `git cl land` upload '
4129 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004130 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4131 options.verbose)
4132
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004133 current = cl.GetBranch()
4134 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4135 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print()
4137 print('Attempting to push branch %r into another local branch!' % current)
4138 print()
4139 print('Either reparent this branch on top of origin/master:')
4140 print(' git reparent-branch --root')
4141 print()
4142 print('OR run `git rebase-update` if you think the parent branch is ')
4143 print('already committed.')
4144 print()
4145 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004146 return 1
4147
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004148 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004149 # Default to merging against our best guess of the upstream branch.
4150 args = [cl.GetUpstreamBranch()]
4151
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004152 if options.contributor:
4153 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004154 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004155 return 1
4156
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004157 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004158 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004159
sbc@chromium.org71437c02015-04-09 19:29:40 +00004160 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004161 return 1
4162
4163 # This rev-list syntax means "show all commits not in my branch that
4164 # are in base_branch".
4165 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4166 base_branch]).splitlines()
4167 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print('Base branch "%s" has %d commits '
4169 'not in this branch.' % (base_branch, len(upstream_commits)))
4170 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171 return 1
4172
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004173 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004174 svn_head = None
4175 if cmd == 'dcommit' or base_has_submodules:
4176 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4177 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004178
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004179 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004180 # If the base_head is a submodule merge commit, the first parent of the
4181 # base_head should be a git-svn commit, which is what we're interested in.
4182 base_svn_head = base_branch
4183 if base_has_submodules:
4184 base_svn_head += '^1'
4185
4186 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004187 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('This branch has %d additional commits not upstreamed yet.'
4189 % len(extra_commits.splitlines()))
4190 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4191 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004192 return 1
4193
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004194 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004195 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004196 author = None
4197 if options.contributor:
4198 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004199 hook_results = cl.RunHook(
4200 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004201 may_prompt=not options.force,
4202 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004203 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004204 if not hook_results.should_continue():
4205 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004206
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004207 # Check the tree status if the tree status URL is set.
4208 status = GetTreeStatus()
4209 if 'closed' == status:
4210 print('The tree is closed. Please wait for it to reopen. Use '
4211 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4212 return 1
4213 elif 'unknown' == status:
4214 print('Unable to determine tree status. Please verify manually and '
4215 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4216 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004217
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004218 change_desc = ChangeDescription(options.message)
4219 if not change_desc.description and cl.GetIssue():
4220 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004222 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004223 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004224 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004225 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004226 print('No description set.')
4227 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004228 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004230 # Keep a separate copy for the commit message, because the commit message
4231 # contains the link to the Rietveld issue, while the Rietveld message contains
4232 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004233 # Keep a separate copy for the commit message.
4234 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004235 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004236
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004237 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004238 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004239 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004240 # after it. Add a period on a new line to circumvent this. Also add a space
4241 # before the period to make sure that Gitiles continues to correctly resolve
4242 # the URL.
4243 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004244 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004245 commit_desc.append_footer('Patch from %s.' % options.contributor)
4246
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004247 print('Description:')
4248 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004250 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004252 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004253
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004254 # We want to squash all this branch's commits into one commit with the proper
4255 # description. We do this by doing a "reset --soft" to the base branch (which
4256 # keeps the working copy the same), then dcommitting that. If origin/master
4257 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4258 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004259 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004260 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4261 # Delete the branches if they exist.
4262 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4263 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4264 result = RunGitWithCode(showref_cmd)
4265 if result[0] == 0:
4266 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267
4268 # We might be in a directory that's present in this branch but not in the
4269 # trunk. Move up to the top of the tree so that git commands that expect a
4270 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004271 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004272 if rel_base_path:
4273 os.chdir(rel_base_path)
4274
4275 # Stuff our change into the merge branch.
4276 # We wrap in a try...finally block so if anything goes wrong,
4277 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004278 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004279 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004280 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004281 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004282 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004283 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004284 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004286 RunGit(
4287 [
4288 'commit', '--author', options.contributor,
4289 '-m', commit_desc.description,
4290 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004291 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004292 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004293 if base_has_submodules:
4294 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4295 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4296 RunGit(['checkout', CHERRY_PICK_BRANCH])
4297 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004298 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004299 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004300 mirror = settings.GetGitMirror(remote)
4301 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004302 pending_prefix = settings.GetPendingRefPrefix()
4303 if not pending_prefix or branch.startswith(pending_prefix):
4304 # If not using refs/pending/heads/* at all, or target ref is already set
4305 # to pending, then push to the target ref directly.
4306 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004307 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004308 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004309 else:
4310 # Cherry-pick the change on top of pending ref and then push it.
4311 assert branch.startswith('refs/'), branch
4312 assert pending_prefix[-1] == '/', pending_prefix
4313 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004314 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004315 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004316 if retcode == 0:
4317 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 else:
4319 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004320 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004321 'svn', 'dcommit',
4322 '-C%s' % options.similarity,
4323 '--no-rebase', '--rmdir',
4324 ]
4325 if settings.GetForceHttpsCommitUrl():
4326 # Allow forcing https commit URLs for some projects that don't allow
4327 # committing to http URLs (like Google Code).
4328 remote_url = cl.GetGitSvnRemoteUrl()
4329 if urlparse.urlparse(remote_url).scheme == 'http':
4330 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004331 cmd_args.append('--commit-url=%s' % remote_url)
4332 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004333 if 'Committed r' in output:
4334 revision = re.match(
4335 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4336 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337 finally:
4338 # And then swap back to the original branch and clean up.
4339 RunGit(['checkout', '-q', cl.GetBranch()])
4340 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004341 if base_has_submodules:
4342 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004343
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004344 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004345 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004346 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004347
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004348 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004349 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004350 try:
4351 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4352 # We set pushed_to_pending to False, since it made it all the way to the
4353 # real ref.
4354 pushed_to_pending = False
4355 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004356 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004358 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004359 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004361 if not to_pending:
4362 if viewvc_url and revision:
4363 change_desc.append_footer(
4364 'Committed: %s%s' % (viewvc_url, revision))
4365 elif revision:
4366 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004367 print('Closing issue '
4368 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004369 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004370 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004371 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004372 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004373 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004374 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004375 if options.bypass_hooks:
4376 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4377 else:
4378 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004379 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004380
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004381 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004382 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('The commit is in the pending queue (%s).' % pending_ref)
4384 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4385 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004386
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004387 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4388 if os.path.isfile(hook):
4389 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004390
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004391 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392
4393
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004394def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004395 print()
4396 print('Waiting for commit to be landed on %s...' % real_ref)
4397 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004398 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4399 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004400 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004401
4402 loop = 0
4403 while True:
4404 sys.stdout.write('fetching (%d)... \r' % loop)
4405 sys.stdout.flush()
4406 loop += 1
4407
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004408 if mirror:
4409 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004410 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4411 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4412 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4413 for commit in commits.splitlines():
4414 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004415 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004416 return commit
4417
4418 current_rev = to_rev
4419
4420
tandriibf429402016-09-14 07:09:12 -07004421def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004422 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4423
4424 Returns:
4425 (retcode of last operation, output log of last operation).
4426 """
4427 assert pending_ref.startswith('refs/'), pending_ref
4428 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4429 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4430 code = 0
4431 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004432 max_attempts = 3
4433 attempts_left = max_attempts
4434 while attempts_left:
4435 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004436 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004437 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004438
4439 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004440 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004441 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004442 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004443 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004444 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004445 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004446 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004447 continue
4448
4449 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004451 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004452 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004453 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004454 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4455 'the following files have merge conflicts:' % pending_ref)
4456 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4457 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004458 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004459 return code, out
4460
4461 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004462 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004463 code, out = RunGitWithCode(
4464 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4465 if code == 0:
4466 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004467 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004468 return code, out
4469
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004471 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004472 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004473 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004474 print('Fatal push error. Make sure your .netrc credentials and git '
4475 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004476 return code, out
4477
vapiera7fbd5a2016-06-16 09:17:49 -07004478 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004479 return code, out
4480
4481
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004482def IsFatalPushFailure(push_stdout):
4483 """True if retrying push won't help."""
4484 return '(prohibited by Gerrit)' in push_stdout
4485
4486
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004487@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004488def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004489 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004491 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004492 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004493 message = """This repository appears to be a git-svn mirror, but we
4494don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004495 else:
4496 message = """This doesn't appear to be an SVN repository.
4497If your project has a true, writeable git repository, you probably want to run
4498'git cl land' instead.
4499If your project has a git mirror of an upstream SVN master, you probably need
4500to run 'git svn init'.
4501
4502Using the wrong command might cause your commit to appear to succeed, and the
4503review to be closed, without actually landing upstream. If you choose to
4504proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004505 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004506 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004507 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4508 'Please let us know of this project you are committing to:'
4509 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004510 return SendUpstream(parser, args, 'dcommit')
4511
4512
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004513@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004514def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004515 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004516 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004517 print('This appears to be an SVN repository.')
4518 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004519 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004520 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004521 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004522
4523
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004524@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004525def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004526 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004527 parser.add_option('-b', dest='newbranch',
4528 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004529 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004531 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4532 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004533 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004534 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004535 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004536 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004538 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004539
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004540
4541 group = optparse.OptionGroup(
4542 parser,
4543 'Options for continuing work on the current issue uploaded from a '
4544 'different clone (e.g. different machine). Must be used independently '
4545 'from the other options. No issue number should be specified, and the '
4546 'branch must have an issue number associated with it')
4547 group.add_option('--reapply', action='store_true', dest='reapply',
4548 help='Reset the branch and reapply the issue.\n'
4549 'CAUTION: This will undo any local changes in this '
4550 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004551
4552 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004553 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004554 parser.add_option_group(group)
4555
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004556 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004557 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004558 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004559 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004560 auth_config = auth.extract_auth_config_from_options(options)
4561
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004562
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004563 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004564 if options.newbranch:
4565 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004566 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004567 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004568
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004569 cl = Changelist(auth_config=auth_config,
4570 codereview=options.forced_codereview)
4571 if not cl.GetIssue():
4572 parser.error('current branch must have an associated issue')
4573
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004574 upstream = cl.GetUpstreamBranch()
4575 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004576 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004577
4578 RunGit(['reset', '--hard', upstream])
4579 if options.pull:
4580 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004581
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004582 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4583 options.directory)
4584
4585 if len(args) != 1 or not args[0]:
4586 parser.error('Must specify issue number or url')
4587
4588 # We don't want uncommitted changes mixed up with the patch.
4589 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004590 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004592 if options.newbranch:
4593 if options.force:
4594 RunGit(['branch', '-D', options.newbranch],
4595 stderr=subprocess2.PIPE, error_ok=True)
4596 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004597 elif not GetCurrentBranch():
4598 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004599
4600 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4601
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004602 if cl.IsGerrit():
4603 if options.reject:
4604 parser.error('--reject is not supported with Gerrit codereview.')
4605 if options.nocommit:
4606 parser.error('--nocommit is not supported with Gerrit codereview.')
4607 if options.directory:
4608 parser.error('--directory is not supported with Gerrit codereview.')
4609
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004610 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004611 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612
4613
4614def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004615 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004616 # Provide a wrapper for git svn rebase to help avoid accidental
4617 # git svn dcommit.
4618 # It's the only command that doesn't use parser at all since we just defer
4619 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004620
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004621 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004622
4623
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004624def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004625 """Fetches the tree status and returns either 'open', 'closed',
4626 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004627 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628 if url:
4629 status = urllib2.urlopen(url).read().lower()
4630 if status.find('closed') != -1 or status == '0':
4631 return 'closed'
4632 elif status.find('open') != -1 or status == '1':
4633 return 'open'
4634 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004635 return 'unset'
4636
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004638def GetTreeStatusReason():
4639 """Fetches the tree status from a json url and returns the message
4640 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004641 url = settings.GetTreeStatusUrl()
4642 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 connection = urllib2.urlopen(json_url)
4644 status = json.loads(connection.read())
4645 connection.close()
4646 return status['message']
4647
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004648
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004649def GetBuilderMaster(bot_list):
4650 """For a given builder, fetch the master from AE if available."""
4651 map_url = 'https://builders-map.appspot.com/'
4652 try:
4653 master_map = json.load(urllib2.urlopen(map_url))
4654 except urllib2.URLError as e:
4655 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4656 (map_url, e))
4657 except ValueError as e:
4658 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4659 if not master_map:
4660 return None, 'Failed to build master map.'
4661
4662 result_master = ''
4663 for bot in bot_list:
4664 builder = bot.split(':', 1)[0]
4665 master_list = master_map.get(builder, [])
4666 if not master_list:
4667 return None, ('No matching master for builder %s.' % builder)
4668 elif len(master_list) > 1:
4669 return None, ('The builder name %s exists in multiple masters %s.' %
4670 (builder, master_list))
4671 else:
4672 cur_master = master_list[0]
4673 if not result_master:
4674 result_master = cur_master
4675 elif result_master != cur_master:
4676 return None, 'The builders do not belong to the same master.'
4677 return result_master, None
4678
4679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004681 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004682 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004683 status = GetTreeStatus()
4684 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004685 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004686 return 2
4687
vapiera7fbd5a2016-06-16 09:17:49 -07004688 print('The tree is %s' % status)
4689 print()
4690 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691 if status != 'open':
4692 return 1
4693 return 0
4694
4695
maruel@chromium.org15192402012-09-06 12:38:29 +00004696def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004697 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4698 """
tandrii1838bad2016-10-06 00:10:52 -07004699 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004700 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004701 '-b', '--bot', action='append',
4702 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4703 'times to specify multiple builders. ex: '
4704 '"-b win_rel -b win_layout". See '
4705 'the try server waterfall for the builders name and the tests '
4706 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004707 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004708 '-m', '--master', default='',
4709 help=('Specify a try master where to run the tries.'))
tandriif7b29d42016-10-07 08:45:41 -07004710 # TODO(tandrii,nodir): add -B --bucket flag.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004711 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004712 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004713 help='Revision to use for the try job; default: the revision will '
4714 'be determined by the try recipe that builder runs, which usually '
4715 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004716 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004717 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004718 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004719 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004720 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004721 '--project',
4722 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004723 'in recipe to determine to which repository or directory to '
4724 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004725 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004726 '-p', '--property', dest='properties', action='append', default=[],
4727 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004728 'key2=value2 etc. The value will be treated as '
4729 'json if decodable, or as string otherwise. '
4730 'NOTE: using this may make your try job not usable for CQ, '
4731 'which will then schedule another try job with default properties')
4732 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004733 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004734 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004735 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004736 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4737 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004738 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004739 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004740 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004741 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004742
machenbach@chromium.org45453142015-09-15 08:45:22 +00004743 # Make sure that all properties are prop=value pairs.
4744 bad_params = [x for x in options.properties if '=' not in x]
4745 if bad_params:
4746 parser.error('Got properties with missing "=": %s' % bad_params)
4747
maruel@chromium.org15192402012-09-06 12:38:29 +00004748 if args:
4749 parser.error('Unknown arguments: %s' % args)
4750
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004751 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004752 if not cl.GetIssue():
4753 parser.error('Need to upload first')
4754
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004755 if cl.IsGerrit():
4756 parser.error(
4757 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4758 'If your project has Commit Queue, dry run is a workaround:\n'
4759 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004760
tandriie113dfd2016-10-11 10:20:12 -07004761 error_message = cl.CannotTriggerTryJobReason()
4762 if error_message:
4763 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004764
maruel@chromium.org15192402012-09-06 12:38:29 +00004765 if not options.name:
4766 options.name = cl.GetBranch()
4767
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004768 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004769 options.master, err_msg = GetBuilderMaster(options.bot)
4770 if err_msg:
4771 parser.error('Tryserver master cannot be found because: %s\n'
4772 'Please manually specify the tryserver master'
4773 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004774
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004775 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004776 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004777 if not options.bot:
4778 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004779
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004780 # Get try masters from PRESUBMIT.py files.
4781 masters = presubmit_support.DoGetTryMasters(
4782 change,
4783 change.LocalPaths(),
4784 settings.GetRoot(),
4785 None,
4786 None,
4787 options.verbose,
4788 sys.stdout)
4789 if masters:
4790 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004791
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004792 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4793 options.bot = presubmit_support.DoGetTrySlaves(
4794 change,
4795 change.LocalPaths(),
4796 settings.GetRoot(),
4797 None,
4798 None,
4799 options.verbose,
4800 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004801
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004802 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004803 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004804
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004805 builders_and_tests = {}
4806 # TODO(machenbach): The old style command-line options don't support
4807 # multiple try masters yet.
4808 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4809 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4810
4811 for bot in old_style:
4812 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004813 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004814 elif ',' in bot:
4815 parser.error('Specify one bot per --bot flag')
4816 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004817 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004818
4819 for bot, tests in new_style:
4820 builders_and_tests.setdefault(bot, []).extend(tests)
4821
4822 # Return a master map with one master to be backwards compatible. The
4823 # master name defaults to an empty string, which will cause the master
4824 # not to be set on rietveld (deprecated).
4825 return {options.master: builders_and_tests}
4826
4827 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004828 if not masters:
4829 # Default to triggering Dry Run (see http://crbug.com/625697).
4830 if options.verbose:
4831 print('git cl try with no bots now defaults to CQ Dry Run.')
4832 try:
4833 cl.SetCQState(_CQState.DRY_RUN)
4834 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4835 return 0
4836 except KeyboardInterrupt:
4837 raise
4838 except:
4839 print('WARNING: failed to trigger CQ Dry Run.\n'
4840 'Either:\n'
4841 ' * your project has no CQ\n'
4842 ' * you don\'t have permission to trigger Dry Run\n'
4843 ' * bug in this code (see stack trace below).\n'
4844 'Consider specifying which bots to trigger manually '
4845 'or asking your project owners for permissions '
4846 'or contacting Chrome Infrastructure team at '
4847 'https://www.chromium.org/infra\n\n')
4848 # Still raise exception so that stack trace is printed.
4849 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004850
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004851 for builders in masters.itervalues():
4852 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004853 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004854 'of bot requires an initial job from a parent (usually a builder). '
4855 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004856 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004857 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004858
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004859 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004860 if patchset != cl.GetPatchset():
4861 print('Warning: Codereview server has newer patchsets (%s) than most '
4862 'recent upload from local checkout (%s). Did a previous upload '
4863 'fail?\n'
4864 'By default, git cl try uses the latest patchset from '
4865 'codereview, continuing to use patchset %s.\n' %
4866 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004867 try:
tandriide281ae2016-10-12 06:02:30 -07004868 _trigger_try_jobs(auth_config, cl, masters, options, 'git_cl_try',
4869 patchset)
tandrii568043b2016-10-11 07:49:18 -07004870 except BuildbucketResponseException as ex:
4871 print('ERROR: %s' % ex)
4872 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004873 return 0
4874
4875
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004876def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004877 """Prints info about try jobs associated with current CL."""
4878 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004879 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004880 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004881 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004882 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004883 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004884 '--color', action='store_true', default=setup_color.IS_TTY,
4885 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004886 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004887 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4888 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004889 group.add_option(
4890 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004891 parser.add_option_group(group)
4892 auth.add_auth_options(parser)
4893 options, args = parser.parse_args(args)
4894 if args:
4895 parser.error('Unrecognized args: %s' % ' '.join(args))
4896
4897 auth_config = auth.extract_auth_config_from_options(options)
4898 cl = Changelist(auth_config=auth_config)
4899 if not cl.GetIssue():
4900 parser.error('Need to upload first')
4901
tandrii221ab252016-10-06 08:12:04 -07004902 patchset = options.patchset
4903 if not patchset:
4904 patchset = cl.GetMostRecentPatchset()
4905 if not patchset:
4906 parser.error('Codereview doesn\'t know about issue %s. '
4907 'No access to issue or wrong issue number?\n'
4908 'Either upload first, or pass --patchset explicitely' %
4909 cl.GetIssue())
4910
4911 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004912 print('Warning: Codereview server has newer patchsets (%s) than most '
4913 'recent upload from local checkout (%s). Did a previous upload '
4914 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004915 'By default, git cl try-results uses the latest patchset from '
4916 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004917 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004918 try:
tandrii221ab252016-10-06 08:12:04 -07004919 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004920 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004921 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004922 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004923 if options.json:
4924 write_try_results_json(options.json, jobs)
4925 else:
4926 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004927 return 0
4928
4929
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004930@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004931def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004932 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004933 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004934 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004935 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004936
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004937 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004938 if args:
4939 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004940 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004941 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004942 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004943 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004944
4945 # Clear configured merge-base, if there is one.
4946 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004947 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004948 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004949 return 0
4950
4951
thestig@chromium.org00858c82013-12-02 23:08:03 +00004952def CMDweb(parser, args):
4953 """Opens the current CL in the web browser."""
4954 _, args = parser.parse_args(args)
4955 if args:
4956 parser.error('Unrecognized args: %s' % ' '.join(args))
4957
4958 issue_url = Changelist().GetIssueURL()
4959 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004960 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004961 return 1
4962
4963 webbrowser.open(issue_url)
4964 return 0
4965
4966
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004967def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004968 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004969 parser.add_option('-d', '--dry-run', action='store_true',
4970 help='trigger in dry run mode')
4971 parser.add_option('-c', '--clear', action='store_true',
4972 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004973 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004974 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004975 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004976 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004977 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004978 if args:
4979 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004980 if options.dry_run and options.clear:
4981 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4982
iannuccie53c9352016-08-17 14:40:40 -07004983 cl = Changelist(auth_config=auth_config, issue=options.issue,
4984 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004985 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004986 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004987 elif options.dry_run:
4988 state = _CQState.DRY_RUN
4989 else:
4990 state = _CQState.COMMIT
4991 if not cl.GetIssue():
4992 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004993 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004994 return 0
4995
4996
groby@chromium.org411034a2013-02-26 15:12:01 +00004997def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004998 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004999 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005000 auth.add_auth_options(parser)
5001 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005002 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005003 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005004 if args:
5005 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005006 cl = Changelist(auth_config=auth_config, issue=options.issue,
5007 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005008 # Ensure there actually is an issue to close.
5009 cl.GetDescription()
5010 cl.CloseIssue()
5011 return 0
5012
5013
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005014def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005015 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005016 parser.add_option(
5017 '--stat',
5018 action='store_true',
5019 dest='stat',
5020 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005021 auth.add_auth_options(parser)
5022 options, args = parser.parse_args(args)
5023 auth_config = auth.extract_auth_config_from_options(options)
5024 if args:
5025 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005026
5027 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005028 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005029 # Staged changes would be committed along with the patch from last
5030 # upload, hence counted toward the "last upload" side in the final
5031 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005032 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005033 return 1
5034
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005035 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005036 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005037 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005038 if not issue:
5039 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005040 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005041 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005042
5043 # Create a new branch based on the merge-base
5044 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005045 # Clear cached branch in cl object, to avoid overwriting original CL branch
5046 # properties.
5047 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005048 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005049 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005050 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005051 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005052 return rtn
5053
wychen@chromium.org06928532015-02-03 02:11:29 +00005054 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005055 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005056 cmd = ['git', 'diff']
5057 if options.stat:
5058 cmd.append('--stat')
5059 cmd.extend([TMP_BRANCH, branch, '--'])
5060 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005061 finally:
5062 RunGit(['checkout', '-q', branch])
5063 RunGit(['branch', '-D', TMP_BRANCH])
5064
5065 return 0
5066
5067
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005068def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005069 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005070 parser.add_option(
5071 '--no-color',
5072 action='store_true',
5073 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005074 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005075 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005076 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005077
5078 author = RunGit(['config', 'user.email']).strip() or None
5079
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005080 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005081
5082 if args:
5083 if len(args) > 1:
5084 parser.error('Unknown args')
5085 base_branch = args[0]
5086 else:
5087 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005088 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005089
5090 change = cl.GetChange(base_branch, None)
5091 return owners_finder.OwnersFinder(
5092 [f.LocalPath() for f in
5093 cl.GetChange(base_branch, None).AffectedFiles()],
5094 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005095 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005096 disable_color=options.no_color).run()
5097
5098
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005099def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005100 """Generates a diff command."""
5101 # Generate diff for the current branch's changes.
5102 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5103 upstream_commit, '--' ]
5104
5105 if args:
5106 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005107 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005108 diff_cmd.append(arg)
5109 else:
5110 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005111
5112 return diff_cmd
5113
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005114def MatchingFileType(file_name, extensions):
5115 """Returns true if the file name ends with one of the given extensions."""
5116 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005117
enne@chromium.org555cfe42014-01-29 18:21:39 +00005118@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005119def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005120 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005121 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005122 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005123 parser.add_option('--full', action='store_true',
5124 help='Reformat the full content of all touched files')
5125 parser.add_option('--dry-run', action='store_true',
5126 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005127 parser.add_option('--python', action='store_true',
5128 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005129 parser.add_option('--diff', action='store_true',
5130 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005131 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005132
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005133 # git diff generates paths against the root of the repository. Change
5134 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005135 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005136 if rel_base_path:
5137 os.chdir(rel_base_path)
5138
digit@chromium.org29e47272013-05-17 17:01:46 +00005139 # Grab the merge-base commit, i.e. the upstream commit of the current
5140 # branch when it was created or the last time it was rebased. This is
5141 # to cover the case where the user may have called "git fetch origin",
5142 # moving the origin branch to a newer commit, but hasn't rebased yet.
5143 upstream_commit = None
5144 cl = Changelist()
5145 upstream_branch = cl.GetUpstreamBranch()
5146 if upstream_branch:
5147 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5148 upstream_commit = upstream_commit.strip()
5149
5150 if not upstream_commit:
5151 DieWithError('Could not find base commit for this branch. '
5152 'Are you in detached state?')
5153
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005154 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5155 diff_output = RunGit(changed_files_cmd)
5156 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005157 # Filter out files deleted by this CL
5158 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005159
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005160 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5161 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5162 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005163 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005164
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005165 top_dir = os.path.normpath(
5166 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5167
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005168 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5169 # formatted. This is used to block during the presubmit.
5170 return_value = 0
5171
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005172 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005173 # Locate the clang-format binary in the checkout
5174 try:
5175 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005176 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005177 DieWithError(e)
5178
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005179 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005180 cmd = [clang_format_tool]
5181 if not opts.dry_run and not opts.diff:
5182 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005183 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005184 if opts.diff:
5185 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005186 else:
5187 env = os.environ.copy()
5188 env['PATH'] = str(os.path.dirname(clang_format_tool))
5189 try:
5190 script = clang_format.FindClangFormatScriptInChromiumTree(
5191 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005192 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005193 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005194
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005195 cmd = [sys.executable, script, '-p0']
5196 if not opts.dry_run and not opts.diff:
5197 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005198
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005199 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5200 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005201
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005202 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5203 if opts.diff:
5204 sys.stdout.write(stdout)
5205 if opts.dry_run and len(stdout) > 0:
5206 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005207
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005208 # Similar code to above, but using yapf on .py files rather than clang-format
5209 # on C/C++ files
5210 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005211 yapf_tool = gclient_utils.FindExecutable('yapf')
5212 if yapf_tool is None:
5213 DieWithError('yapf not found in PATH')
5214
5215 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005216 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005217 cmd = [yapf_tool]
5218 if not opts.dry_run and not opts.diff:
5219 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005220 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005221 if opts.diff:
5222 sys.stdout.write(stdout)
5223 else:
5224 # TODO(sbc): yapf --lines mode still has some issues.
5225 # https://github.com/google/yapf/issues/154
5226 DieWithError('--python currently only works with --full')
5227
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005228 # Dart's formatter does not have the nice property of only operating on
5229 # modified chunks, so hard code full.
5230 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005231 try:
5232 command = [dart_format.FindDartFmtToolInChromiumTree()]
5233 if not opts.dry_run and not opts.diff:
5234 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005235 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005236
ppi@chromium.org6593d932016-03-03 15:41:15 +00005237 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005238 if opts.dry_run and stdout:
5239 return_value = 2
5240 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005241 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5242 'found in this checkout. Files in other languages are still '
5243 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005244
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005245 # Format GN build files. Always run on full build files for canonical form.
5246 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005247 cmd = ['gn', 'format' ]
5248 if opts.dry_run or opts.diff:
5249 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005250 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005251 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5252 shell=sys.platform == 'win32',
5253 cwd=top_dir)
5254 if opts.dry_run and gn_ret == 2:
5255 return_value = 2 # Not formatted.
5256 elif opts.diff and gn_ret == 2:
5257 # TODO this should compute and print the actual diff.
5258 print("This change has GN build file diff for " + gn_diff_file)
5259 elif gn_ret != 0:
5260 # For non-dry run cases (and non-2 return values for dry-run), a
5261 # nonzero error code indicates a failure, probably because the file
5262 # doesn't parse.
5263 DieWithError("gn format failed on " + gn_diff_file +
5264 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005265
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005266 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005267
5268
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005269@subcommand.usage('<codereview url or issue id>')
5270def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005271 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005272 _, args = parser.parse_args(args)
5273
5274 if len(args) != 1:
5275 parser.print_help()
5276 return 1
5277
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005278 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005279 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005280 parser.print_help()
5281 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005282 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005283
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005284 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005285 output = RunGit(['config', '--local', '--get-regexp',
5286 r'branch\..*\.%s' % issueprefix],
5287 error_ok=True)
5288 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005289 if issue == target_issue:
5290 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005291
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005292 branches = []
5293 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005294 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005295 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005296 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005297 return 1
5298 if len(branches) == 1:
5299 RunGit(['checkout', branches[0]])
5300 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005301 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005302 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005303 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005304 which = raw_input('Choose by index: ')
5305 try:
5306 RunGit(['checkout', branches[int(which)]])
5307 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005308 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005309 return 1
5310
5311 return 0
5312
5313
maruel@chromium.org29404b52014-09-08 22:58:00 +00005314def CMDlol(parser, args):
5315 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005316 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005317 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5318 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5319 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005320 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005321 return 0
5322
5323
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005324class OptionParser(optparse.OptionParser):
5325 """Creates the option parse and add --verbose support."""
5326 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005327 optparse.OptionParser.__init__(
5328 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005329 self.add_option(
5330 '-v', '--verbose', action='count', default=0,
5331 help='Use 2 times for more debugging info')
5332
5333 def parse_args(self, args=None, values=None):
5334 options, args = optparse.OptionParser.parse_args(self, args, values)
5335 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5336 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5337 return options, args
5338
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005340def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005341 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005342 print('\nYour python version %s is unsupported, please upgrade.\n' %
5343 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005344 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005345
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005346 # Reload settings.
5347 global settings
5348 settings = Settings()
5349
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005350 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005351 dispatcher = subcommand.CommandDispatcher(__name__)
5352 try:
5353 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005354 except auth.AuthenticationError as e:
5355 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005356 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005357 if e.code != 500:
5358 raise
5359 DieWithError(
5360 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5361 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005362 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005363
5364
5365if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005366 # These affect sys.stdout so do it outside of main() to simplify mocks in
5367 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005368 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005369 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005370 try:
5371 sys.exit(main(sys.argv[1:]))
5372 except KeyboardInterrupt:
5373 sys.stderr.write('interrupted\n')
5374 sys.exit(1)