blob: a61bb9144687a796f002f51cbce6c48299ccbfa5 [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:
rmistryc9ebbd22016-10-14 12:35:54 -07002340 votes = cq_label.get('all', [])
2341 highest_vote = 0
2342 for v in votes:
2343 highest_vote = max(highest_vote, v.get('value', 0))
2344 vote_value = str(highest_vote)
2345 if vote_value != '0':
2346 # Add a '+' if the value is not 0 to match the values in the label.
2347 # The cq_label does not have negatives.
2348 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002349 vote_text = cq_label.get('values', {}).get(vote_value, '')
2350 if vote_text.lower() == 'commit':
2351 return 'commit'
2352
2353 lgtm_label = data['labels'].get('Code-Review', {})
2354 if lgtm_label:
2355 if 'rejected' in lgtm_label:
2356 return 'not lgtm'
2357 if 'approved' in lgtm_label:
2358 return 'lgtm'
2359
2360 if not data.get('reviewers', {}).get('REVIEWER', []):
2361 return 'unsent'
2362
2363 messages = data.get('messages', [])
2364 if messages:
2365 owner = data['owner'].get('_account_id')
2366 last_message_author = messages[-1].get('author', {}).get('_account_id')
2367 if owner != last_message_author:
2368 # Some reply from non-owner.
2369 return 'reply'
2370
2371 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002372
2373 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002374 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375 return data['revisions'][data['current_revision']]['_number']
2376
2377 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002378 data = self._GetChangeDetail(['CURRENT_REVISION'])
2379 current_rev = data['current_revision']
2380 url = data['revisions'][current_rev]['fetch']['http']['url']
2381 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002382
dsansomee2d6fd92016-09-08 00:10:47 -07002383 def UpdateDescriptionRemote(self, description, force=False):
2384 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2385 if not force:
2386 ask_for_data(
2387 'The description cannot be modified while the issue has a pending '
2388 'unpublished edit. Either publish the edit in the Gerrit web UI '
2389 'or delete it.\n\n'
2390 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2391
2392 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2393 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002394 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2395 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002396
2397 def CloseIssue(self):
2398 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2399
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002400 def GetApprovingReviewers(self):
2401 """Returns a list of reviewers approving the change.
2402
2403 Note: not necessarily committers.
2404 """
2405 raise NotImplementedError()
2406
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002407 def SubmitIssue(self, wait_for_merge=True):
2408 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2409 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002410
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002411 def _GetChangeDetail(self, options=None, issue=None):
2412 options = options or []
2413 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002414 assert issue, 'issue is required to query Gerrit'
2415 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002416 options)
tandriic2405f52016-10-10 08:13:15 -07002417 if not data:
2418 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2419 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002420
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002421 def CMDLand(self, force, bypass_hooks, verbose):
2422 if git_common.is_dirty_git_tree('land'):
2423 return 1
tandriid60367b2016-06-22 05:25:12 -07002424 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2425 if u'Commit-Queue' in detail.get('labels', {}):
2426 if not force:
2427 ask_for_data('\nIt seems this repository has a Commit Queue, '
2428 'which can test and land changes for you. '
2429 'Are you sure you wish to bypass it?\n'
2430 'Press Enter to continue, Ctrl+C to abort.')
2431
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002432 differs = True
tandriic4344b52016-08-29 06:04:54 -07002433 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002434 # Note: git diff outputs nothing if there is no diff.
2435 if not last_upload or RunGit(['diff', last_upload]).strip():
2436 print('WARNING: some changes from local branch haven\'t been uploaded')
2437 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002438 if detail['current_revision'] == last_upload:
2439 differs = False
2440 else:
2441 print('WARNING: local branch contents differ from latest uploaded '
2442 'patchset')
2443 if differs:
2444 if not force:
2445 ask_for_data(
2446 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2447 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2448 elif not bypass_hooks:
2449 hook_results = self.RunHook(
2450 committing=True,
2451 may_prompt=not force,
2452 verbose=verbose,
2453 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2454 if not hook_results.should_continue():
2455 return 1
2456
2457 self.SubmitIssue(wait_for_merge=True)
2458 print('Issue %s has been submitted.' % self.GetIssueURL())
2459 return 0
2460
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002461 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2462 directory):
2463 assert not reject
2464 assert not nocommit
2465 assert not directory
2466 assert parsed_issue_arg.valid
2467
2468 self._changelist.issue = parsed_issue_arg.issue
2469
2470 if parsed_issue_arg.hostname:
2471 self._gerrit_host = parsed_issue_arg.hostname
2472 self._gerrit_server = 'https://%s' % self._gerrit_host
2473
tandriic2405f52016-10-10 08:13:15 -07002474 try:
2475 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2476 except GerritIssueNotExists as e:
2477 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002478
2479 if not parsed_issue_arg.patchset:
2480 # Use current revision by default.
2481 revision_info = detail['revisions'][detail['current_revision']]
2482 patchset = int(revision_info['_number'])
2483 else:
2484 patchset = parsed_issue_arg.patchset
2485 for revision_info in detail['revisions'].itervalues():
2486 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2487 break
2488 else:
2489 DieWithError('Couldn\'t find patchset %i in issue %i' %
2490 (parsed_issue_arg.patchset, self.GetIssue()))
2491
2492 fetch_info = revision_info['fetch']['http']
2493 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2494 RunGit(['cherry-pick', 'FETCH_HEAD'])
2495 self.SetIssue(self.GetIssue())
2496 self.SetPatchset(patchset)
2497 print('Committed patch for issue %i pathset %i locally' %
2498 (self.GetIssue(), self.GetPatchset()))
2499 return 0
2500
2501 @staticmethod
2502 def ParseIssueURL(parsed_url):
2503 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2504 return None
2505 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2506 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2507 # Short urls like https://domain/<issue_number> can be used, but don't allow
2508 # specifying the patchset (you'd 404), but we allow that here.
2509 if parsed_url.path == '/':
2510 part = parsed_url.fragment
2511 else:
2512 part = parsed_url.path
2513 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2514 if match:
2515 return _ParsedIssueNumberArgument(
2516 issue=int(match.group(2)),
2517 patchset=int(match.group(4)) if match.group(4) else None,
2518 hostname=parsed_url.netloc)
2519 return None
2520
tandrii16e0b4e2016-06-07 10:34:28 -07002521 def _GerritCommitMsgHookCheck(self, offer_removal):
2522 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2523 if not os.path.exists(hook):
2524 return
2525 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2526 # custom developer made one.
2527 data = gclient_utils.FileRead(hook)
2528 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2529 return
2530 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002531 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002532 'and may interfere with it in subtle ways.\n'
2533 'We recommend you remove the commit-msg hook.')
2534 if offer_removal:
2535 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2536 if reply.lower().startswith('y'):
2537 gclient_utils.rm_file_or_tree(hook)
2538 print('Gerrit commit-msg hook removed.')
2539 else:
2540 print('OK, will keep Gerrit commit-msg hook in place.')
2541
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542 def CMDUploadChange(self, options, args, change):
2543 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002544 if options.squash and options.no_squash:
2545 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002546
2547 if not options.squash and not options.no_squash:
2548 # Load default for user, repo, squash=true, in this order.
2549 options.squash = settings.GetSquashGerritUploads()
2550 elif options.no_squash:
2551 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002552
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002553 # We assume the remote called "origin" is the one we want.
2554 # It is probably not worthwhile to support different workflows.
2555 gerrit_remote = 'origin'
2556
2557 remote, remote_branch = self.GetRemoteBranch()
2558 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2559 pending_prefix='')
2560
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002561 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002562 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002563 if self.GetIssue():
2564 # Try to get the message from a previous upload.
2565 message = self.GetDescription()
2566 if not message:
2567 DieWithError(
2568 'failed to fetch description from current Gerrit issue %d\n'
2569 '%s' % (self.GetIssue(), self.GetIssueURL()))
2570 change_id = self._GetChangeDetail()['change_id']
2571 while True:
2572 footer_change_ids = git_footers.get_footer_change_id(message)
2573 if footer_change_ids == [change_id]:
2574 break
2575 if not footer_change_ids:
2576 message = git_footers.add_footer_change_id(message, change_id)
2577 print('WARNING: appended missing Change-Id to issue description')
2578 continue
2579 # There is already a valid footer but with different or several ids.
2580 # Doing this automatically is non-trivial as we don't want to lose
2581 # existing other footers, yet we want to append just 1 desired
2582 # Change-Id. Thus, just create a new footer, but let user verify the
2583 # new description.
2584 message = '%s\n\nChange-Id: %s' % (message, change_id)
2585 print(
2586 'WARNING: issue %s has Change-Id footer(s):\n'
2587 ' %s\n'
2588 'but issue has Change-Id %s, according to Gerrit.\n'
2589 'Please, check the proposed correction to the description, '
2590 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2591 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2592 change_id))
2593 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2594 if not options.force:
2595 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002596 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002597 message = change_desc.description
2598 if not message:
2599 DieWithError("Description is empty. Aborting...")
2600 # Continue the while loop.
2601 # Sanity check of this code - we should end up with proper message
2602 # footer.
2603 assert [change_id] == git_footers.get_footer_change_id(message)
2604 change_desc = ChangeDescription(message)
2605 else:
2606 change_desc = ChangeDescription(
2607 options.message or CreateDescriptionFromLog(args))
2608 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002609 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002610 if not change_desc.description:
2611 DieWithError("Description is empty. Aborting...")
2612 message = change_desc.description
2613 change_ids = git_footers.get_footer_change_id(message)
2614 if len(change_ids) > 1:
2615 DieWithError('too many Change-Id footers, at most 1 allowed.')
2616 if not change_ids:
2617 # Generate the Change-Id automatically.
2618 message = git_footers.add_footer_change_id(
2619 message, GenerateGerritChangeId(message))
2620 change_desc.set_description(message)
2621 change_ids = git_footers.get_footer_change_id(message)
2622 assert len(change_ids) == 1
2623 change_id = change_ids[0]
2624
2625 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2626 if remote is '.':
2627 # If our upstream branch is local, we base our squashed commit on its
2628 # squashed version.
2629 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2630 # Check the squashed hash of the parent.
2631 parent = RunGit(['config',
2632 'branch.%s.gerritsquashhash' % upstream_branch_name],
2633 error_ok=True).strip()
2634 # Verify that the upstream branch has been uploaded too, otherwise
2635 # Gerrit will create additional CLs when uploading.
2636 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2637 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002638 DieWithError(
2639 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002640 'Note: maybe you\'ve uploaded it with --no-squash. '
2641 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 ' git cl upload --squash\n' % upstream_branch_name)
2643 else:
2644 parent = self.GetCommonAncestorWithUpstream()
2645
2646 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2647 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2648 '-m', message]).strip()
2649 else:
2650 change_desc = ChangeDescription(
2651 options.message or CreateDescriptionFromLog(args))
2652 if not change_desc.description:
2653 DieWithError("Description is empty. Aborting...")
2654
2655 if not git_footers.get_footer_change_id(change_desc.description):
2656 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002657 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2658 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 ref_to_push = 'HEAD'
2660 parent = '%s/%s' % (gerrit_remote, branch)
2661 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2662
2663 assert change_desc
2664 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2665 ref_to_push)]).splitlines()
2666 if len(commits) > 1:
2667 print('WARNING: This will upload %d commits. Run the following command '
2668 'to see which commits will be uploaded: ' % len(commits))
2669 print('git log %s..%s' % (parent, ref_to_push))
2670 print('You can also use `git squash-branch` to squash these into a '
2671 'single commit.')
2672 ask_for_data('About to upload; enter to confirm.')
2673
2674 if options.reviewers or options.tbr_owners:
2675 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2676 change)
2677
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002678 # Extra options that can be specified at push time. Doc:
2679 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2680 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002681 if change_desc.get_reviewers(tbr_only=True):
2682 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2683 refspec_opts.append('l=Code-Review+1')
2684
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002685 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002686 if not re.match(r'^[\w ]+$', options.title):
2687 options.title = re.sub(r'[^\w ]', '', options.title)
2688 print('WARNING: Patchset title may only contain alphanumeric chars '
2689 'and spaces. Cleaned up title:\n%s' % options.title)
2690 if not options.force:
2691 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002692 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2693 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002694 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2695
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002696 if options.send_mail:
2697 if not change_desc.get_reviewers():
2698 DieWithError('Must specify reviewers to send email.')
2699 refspec_opts.append('notify=ALL')
2700 else:
2701 refspec_opts.append('notify=NONE')
2702
tandrii99a72f22016-08-17 14:33:24 -07002703 reviewers = change_desc.get_reviewers()
2704 if reviewers:
2705 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002706
agablec6787972016-09-09 16:13:34 -07002707 if options.private:
2708 refspec_opts.append('draft')
2709
rmistry9eadede2016-09-19 11:22:43 -07002710 if options.topic:
2711 # Documentation on Gerrit topics is here:
2712 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2713 refspec_opts.append('topic=%s' % options.topic)
2714
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002715 refspec_suffix = ''
2716 if refspec_opts:
2717 refspec_suffix = '%' + ','.join(refspec_opts)
2718 assert ' ' not in refspec_suffix, (
2719 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002720 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002721
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002723 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002724 print_stdout=True,
2725 # Flush after every line: useful for seeing progress when running as
2726 # recipe.
2727 filter_fn=lambda _: sys.stdout.flush())
2728
2729 if options.squash:
2730 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2731 change_numbers = [m.group(1)
2732 for m in map(regex.match, push_stdout.splitlines())
2733 if m]
2734 if len(change_numbers) != 1:
2735 DieWithError(
2736 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2737 'Change-Id: %s') % (len(change_numbers), change_id))
2738 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002739 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002740
2741 # Add cc's from the CC_LIST and --cc flag (if any).
2742 cc = self.GetCCList().split(',')
2743 if options.cc:
2744 cc.extend(options.cc)
2745 cc = filter(None, [email.strip() for email in cc])
2746 if cc:
2747 gerrit_util.AddReviewers(
2748 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2749
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002750 return 0
2751
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002752 def _AddChangeIdToCommitMessage(self, options, args):
2753 """Re-commits using the current message, assumes the commit hook is in
2754 place.
2755 """
2756 log_desc = options.message or CreateDescriptionFromLog(args)
2757 git_command = ['commit', '--amend', '-m', log_desc]
2758 RunGit(git_command)
2759 new_log_desc = CreateDescriptionFromLog(args)
2760 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002761 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002762 return new_log_desc
2763 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002764 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002765
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002766 def SetCQState(self, new_state):
2767 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002768 vote_map = {
2769 _CQState.NONE: 0,
2770 _CQState.DRY_RUN: 1,
2771 _CQState.COMMIT : 2,
2772 }
2773 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2774 labels={'Commit-Queue': vote_map[new_state]})
2775
tandriie113dfd2016-10-11 10:20:12 -07002776 def CannotTriggerTryJobReason(self):
2777 # TODO(tandrii): implement for Gerrit.
2778 raise NotImplementedError()
2779
tandriide281ae2016-10-12 06:02:30 -07002780 def GetIssueOwner(self):
2781 # TODO(tandrii): implement for Gerrit.
2782 raise NotImplementedError()
2783
2784 def GetIssueProject(self):
2785 # TODO(tandrii): implement for Gerrit.
2786 raise NotImplementedError()
2787
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002788
2789_CODEREVIEW_IMPLEMENTATIONS = {
2790 'rietveld': _RietveldChangelistImpl,
2791 'gerrit': _GerritChangelistImpl,
2792}
2793
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002794
iannuccie53c9352016-08-17 14:40:40 -07002795def _add_codereview_issue_select_options(parser, extra=""):
2796 _add_codereview_select_options(parser)
2797
2798 text = ('Operate on this issue number instead of the current branch\'s '
2799 'implicit issue.')
2800 if extra:
2801 text += ' '+extra
2802 parser.add_option('-i', '--issue', type=int, help=text)
2803
2804
2805def _process_codereview_issue_select_options(parser, options):
2806 _process_codereview_select_options(parser, options)
2807 if options.issue is not None and not options.forced_codereview:
2808 parser.error('--issue must be specified with either --rietveld or --gerrit')
2809
2810
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002811def _add_codereview_select_options(parser):
2812 """Appends --gerrit and --rietveld options to force specific codereview."""
2813 parser.codereview_group = optparse.OptionGroup(
2814 parser, 'EXPERIMENTAL! Codereview override options')
2815 parser.add_option_group(parser.codereview_group)
2816 parser.codereview_group.add_option(
2817 '--gerrit', action='store_true',
2818 help='Force the use of Gerrit for codereview')
2819 parser.codereview_group.add_option(
2820 '--rietveld', action='store_true',
2821 help='Force the use of Rietveld for codereview')
2822
2823
2824def _process_codereview_select_options(parser, options):
2825 if options.gerrit and options.rietveld:
2826 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2827 options.forced_codereview = None
2828 if options.gerrit:
2829 options.forced_codereview = 'gerrit'
2830 elif options.rietveld:
2831 options.forced_codereview = 'rietveld'
2832
2833
tandriif9aefb72016-07-01 09:06:51 -07002834def _get_bug_line_values(default_project, bugs):
2835 """Given default_project and comma separated list of bugs, yields bug line
2836 values.
2837
2838 Each bug can be either:
2839 * a number, which is combined with default_project
2840 * string, which is left as is.
2841
2842 This function may produce more than one line, because bugdroid expects one
2843 project per line.
2844
2845 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2846 ['v8:123', 'chromium:789']
2847 """
2848 default_bugs = []
2849 others = []
2850 for bug in bugs.split(','):
2851 bug = bug.strip()
2852 if bug:
2853 try:
2854 default_bugs.append(int(bug))
2855 except ValueError:
2856 others.append(bug)
2857
2858 if default_bugs:
2859 default_bugs = ','.join(map(str, default_bugs))
2860 if default_project:
2861 yield '%s:%s' % (default_project, default_bugs)
2862 else:
2863 yield default_bugs
2864 for other in sorted(others):
2865 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2866 yield other
2867
2868
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002869class ChangeDescription(object):
2870 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002871 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002872 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002873
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002874 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002875 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002876
agable@chromium.org42c20792013-09-12 17:34:49 +00002877 @property # www.logilab.org/ticket/89786
2878 def description(self): # pylint: disable=E0202
2879 return '\n'.join(self._description_lines)
2880
2881 def set_description(self, desc):
2882 if isinstance(desc, basestring):
2883 lines = desc.splitlines()
2884 else:
2885 lines = [line.rstrip() for line in desc]
2886 while lines and not lines[0]:
2887 lines.pop(0)
2888 while lines and not lines[-1]:
2889 lines.pop(-1)
2890 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002891
piman@chromium.org336f9122014-09-04 02:16:55 +00002892 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002893 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002894 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002895 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002896 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002897 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002898
agable@chromium.org42c20792013-09-12 17:34:49 +00002899 # Get the set of R= and TBR= lines and remove them from the desciption.
2900 regexp = re.compile(self.R_LINE)
2901 matches = [regexp.match(line) for line in self._description_lines]
2902 new_desc = [l for i, l in enumerate(self._description_lines)
2903 if not matches[i]]
2904 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002905
agable@chromium.org42c20792013-09-12 17:34:49 +00002906 # Construct new unified R= and TBR= lines.
2907 r_names = []
2908 tbr_names = []
2909 for match in matches:
2910 if not match:
2911 continue
2912 people = cleanup_list([match.group(2).strip()])
2913 if match.group(1) == 'TBR':
2914 tbr_names.extend(people)
2915 else:
2916 r_names.extend(people)
2917 for name in r_names:
2918 if name not in reviewers:
2919 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002920 if add_owners_tbr:
2921 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002922 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002923 all_reviewers = set(tbr_names + reviewers)
2924 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2925 all_reviewers)
2926 tbr_names.extend(owners_db.reviewers_for(missing_files,
2927 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002928 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2929 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2930
2931 # Put the new lines in the description where the old first R= line was.
2932 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2933 if 0 <= line_loc < len(self._description_lines):
2934 if new_tbr_line:
2935 self._description_lines.insert(line_loc, new_tbr_line)
2936 if new_r_line:
2937 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002938 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002939 if new_r_line:
2940 self.append_footer(new_r_line)
2941 if new_tbr_line:
2942 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002943
tandriif9aefb72016-07-01 09:06:51 -07002944 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002945 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002946 self.set_description([
2947 '# Enter a description of the change.',
2948 '# This will be displayed on the codereview site.',
2949 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002950 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002951 '--------------------',
2952 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002953
agable@chromium.org42c20792013-09-12 17:34:49 +00002954 regexp = re.compile(self.BUG_LINE)
2955 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002956 prefix = settings.GetBugPrefix()
2957 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2958 for value in values:
2959 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2960 self.append_footer('BUG=%s' % value)
2961
agable@chromium.org42c20792013-09-12 17:34:49 +00002962 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002963 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002964 if not content:
2965 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002966 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002967
2968 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002969 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2970 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002971 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002972 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002973
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002974 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002975 """Adds a footer line to the description.
2976
2977 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2978 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2979 that Gerrit footers are always at the end.
2980 """
2981 parsed_footer_line = git_footers.parse_footer(line)
2982 if parsed_footer_line:
2983 # Line is a gerrit footer in the form: Footer-Key: any value.
2984 # Thus, must be appended observing Gerrit footer rules.
2985 self.set_description(
2986 git_footers.add_footer(self.description,
2987 key=parsed_footer_line[0],
2988 value=parsed_footer_line[1]))
2989 return
2990
2991 if not self._description_lines:
2992 self._description_lines.append(line)
2993 return
2994
2995 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2996 if gerrit_footers:
2997 # git_footers.split_footers ensures that there is an empty line before
2998 # actual (gerrit) footers, if any. We have to keep it that way.
2999 assert top_lines and top_lines[-1] == ''
3000 top_lines, separator = top_lines[:-1], top_lines[-1:]
3001 else:
3002 separator = [] # No need for separator if there are no gerrit_footers.
3003
3004 prev_line = top_lines[-1] if top_lines else ''
3005 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3006 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3007 top_lines.append('')
3008 top_lines.append(line)
3009 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003010
tandrii99a72f22016-08-17 14:33:24 -07003011 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003012 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003013 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003014 reviewers = [match.group(2).strip()
3015 for match in matches
3016 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003017 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003018
3019
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003020def get_approving_reviewers(props):
3021 """Retrieves the reviewers that approved a CL from the issue properties with
3022 messages.
3023
3024 Note that the list may contain reviewers that are not committer, thus are not
3025 considered by the CQ.
3026 """
3027 return sorted(
3028 set(
3029 message['sender']
3030 for message in props['messages']
3031 if message['approval'] and message['sender'] in props['reviewers']
3032 )
3033 )
3034
3035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003036def FindCodereviewSettingsFile(filename='codereview.settings'):
3037 """Finds the given file starting in the cwd and going up.
3038
3039 Only looks up to the top of the repository unless an
3040 'inherit-review-settings-ok' file exists in the root of the repository.
3041 """
3042 inherit_ok_file = 'inherit-review-settings-ok'
3043 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003044 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3046 root = '/'
3047 while True:
3048 if filename in os.listdir(cwd):
3049 if os.path.isfile(os.path.join(cwd, filename)):
3050 return open(os.path.join(cwd, filename))
3051 if cwd == root:
3052 break
3053 cwd = os.path.dirname(cwd)
3054
3055
3056def LoadCodereviewSettingsFromFile(fileobj):
3057 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003058 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003060 def SetProperty(name, setting, unset_error_ok=False):
3061 fullname = 'rietveld.' + name
3062 if setting in keyvals:
3063 RunGit(['config', fullname, keyvals[setting]])
3064 else:
3065 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3066
3067 SetProperty('server', 'CODE_REVIEW_SERVER')
3068 # Only server setting is required. Other settings can be absent.
3069 # In that case, we ignore errors raised during option deletion attempt.
3070 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003071 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003072 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3073 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003074 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003075 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003076 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3077 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003078 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003079 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003080 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003081 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003082 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3083 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003084
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003085 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003086 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003087
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003088 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003089 RunGit(['config', 'gerrit.squash-uploads',
3090 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003091
tandrii@chromium.org28253532016-04-14 13:46:56 +00003092 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003093 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003094 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003096 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3097 #should be of the form
3098 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3099 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3100 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3101 keyvals['ORIGIN_URL_CONFIG']])
3102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003104def urlretrieve(source, destination):
3105 """urllib is broken for SSL connections via a proxy therefore we
3106 can't use urllib.urlretrieve()."""
3107 with open(destination, 'w') as f:
3108 f.write(urllib2.urlopen(source).read())
3109
3110
ukai@chromium.org712d6102013-11-27 00:52:58 +00003111def hasSheBang(fname):
3112 """Checks fname is a #! script."""
3113 with open(fname) as f:
3114 return f.read(2).startswith('#!')
3115
3116
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003117# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3118def DownloadHooks(*args, **kwargs):
3119 pass
3120
3121
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003122def DownloadGerritHook(force):
3123 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003124
3125 Args:
3126 force: True to update hooks. False to install hooks if not present.
3127 """
3128 if not settings.GetIsGerrit():
3129 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003130 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003131 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3132 if not os.access(dst, os.X_OK):
3133 if os.path.exists(dst):
3134 if not force:
3135 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003136 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003137 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003138 if not hasSheBang(dst):
3139 DieWithError('Not a script: %s\n'
3140 'You need to download from\n%s\n'
3141 'into .git/hooks/commit-msg and '
3142 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003143 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3144 except Exception:
3145 if os.path.exists(dst):
3146 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003147 DieWithError('\nFailed to download hooks.\n'
3148 'You need to download from\n%s\n'
3149 'into .git/hooks/commit-msg and '
3150 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003151
3152
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003153
3154def GetRietveldCodereviewSettingsInteractively():
3155 """Prompt the user for settings."""
3156 server = settings.GetDefaultServerUrl(error_ok=True)
3157 prompt = 'Rietveld server (host[:port])'
3158 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3159 newserver = ask_for_data(prompt + ':')
3160 if not server and not newserver:
3161 newserver = DEFAULT_SERVER
3162 if newserver:
3163 newserver = gclient_utils.UpgradeToHttps(newserver)
3164 if newserver != server:
3165 RunGit(['config', 'rietveld.server', newserver])
3166
3167 def SetProperty(initial, caption, name, is_url):
3168 prompt = caption
3169 if initial:
3170 prompt += ' ("x" to clear) [%s]' % initial
3171 new_val = ask_for_data(prompt + ':')
3172 if new_val == 'x':
3173 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3174 elif new_val:
3175 if is_url:
3176 new_val = gclient_utils.UpgradeToHttps(new_val)
3177 if new_val != initial:
3178 RunGit(['config', 'rietveld.' + name, new_val])
3179
3180 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3181 SetProperty(settings.GetDefaultPrivateFlag(),
3182 'Private flag (rietveld only)', 'private', False)
3183 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3184 'tree-status-url', False)
3185 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3186 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3187 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3188 'run-post-upload-hook', False)
3189
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003190@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003192 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003193
tandrii5d0a0422016-09-14 06:24:35 -07003194 print('WARNING: git cl config works for Rietveld only')
3195 # TODO(tandrii): remove this once we switch to Gerrit.
3196 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003197 parser.add_option('--activate-update', action='store_true',
3198 help='activate auto-updating [rietveld] section in '
3199 '.git/config')
3200 parser.add_option('--deactivate-update', action='store_true',
3201 help='deactivate auto-updating [rietveld] section in '
3202 '.git/config')
3203 options, args = parser.parse_args(args)
3204
3205 if options.deactivate_update:
3206 RunGit(['config', 'rietveld.autoupdate', 'false'])
3207 return
3208
3209 if options.activate_update:
3210 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3211 return
3212
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003214 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003215 return 0
3216
3217 url = args[0]
3218 if not url.endswith('codereview.settings'):
3219 url = os.path.join(url, 'codereview.settings')
3220
3221 # Load code review settings and download hooks (if available).
3222 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3223 return 0
3224
3225
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003226def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003227 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003228 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3229 branch = ShortBranchName(branchref)
3230 _, args = parser.parse_args(args)
3231 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003232 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003233 return RunGit(['config', 'branch.%s.base-url' % branch],
3234 error_ok=False).strip()
3235 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003236 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003237 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3238 error_ok=False).strip()
3239
3240
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003241def color_for_status(status):
3242 """Maps a Changelist status to color, for CMDstatus and other tools."""
3243 return {
3244 'unsent': Fore.RED,
3245 'waiting': Fore.BLUE,
3246 'reply': Fore.YELLOW,
3247 'lgtm': Fore.GREEN,
3248 'commit': Fore.MAGENTA,
3249 'closed': Fore.CYAN,
3250 'error': Fore.WHITE,
3251 }.get(status, Fore.WHITE)
3252
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003253
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003254def get_cl_statuses(changes, fine_grained, max_processes=None):
3255 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003256
3257 If fine_grained is true, this will fetch CL statuses from the server.
3258 Otherwise, simply indicate if there's a matching url for the given branches.
3259
3260 If max_processes is specified, it is used as the maximum number of processes
3261 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3262 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003263
3264 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003265 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003266 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003267 upload.verbosity = 0
3268
3269 if fine_grained:
3270 # Process one branch synchronously to work through authentication, then
3271 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003272 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003273 def fetch(cl):
3274 try:
3275 return (cl, cl.GetStatus())
3276 except:
3277 # See http://crbug.com/629863.
3278 logging.exception('failed to fetch status for %s:', cl)
3279 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003280 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003281
tandriiea9514a2016-08-17 12:32:37 -07003282 changes_to_fetch = changes[1:]
3283 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003284 # Exit early if there was only one branch to fetch.
3285 return
3286
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003287 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003288 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003289 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003290 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003291
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003292 fetched_cls = set()
3293 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003294 while True:
3295 try:
3296 row = it.next(timeout=5)
3297 except multiprocessing.TimeoutError:
3298 break
3299
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003300 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003301 yield row
3302
3303 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003304 for cl in set(changes_to_fetch) - fetched_cls:
3305 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003306
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003307 else:
3308 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003309 for cl in changes:
3310 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003311
rmistry@google.com2dd99862015-06-22 12:22:18 +00003312
3313def upload_branch_deps(cl, args):
3314 """Uploads CLs of local branches that are dependents of the current branch.
3315
3316 If the local branch dependency tree looks like:
3317 test1 -> test2.1 -> test3.1
3318 -> test3.2
3319 -> test2.2 -> test3.3
3320
3321 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3322 run on the dependent branches in this order:
3323 test2.1, test3.1, test3.2, test2.2, test3.3
3324
3325 Note: This function does not rebase your local dependent branches. Use it when
3326 you make a change to the parent branch that will not conflict with its
3327 dependent branches, and you would like their dependencies updated in
3328 Rietveld.
3329 """
3330 if git_common.is_dirty_git_tree('upload-branch-deps'):
3331 return 1
3332
3333 root_branch = cl.GetBranch()
3334 if root_branch is None:
3335 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3336 'Get on a branch!')
3337 if not cl.GetIssue() or not cl.GetPatchset():
3338 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3339 'patchset dependencies without an uploaded CL.')
3340
3341 branches = RunGit(['for-each-ref',
3342 '--format=%(refname:short) %(upstream:short)',
3343 'refs/heads'])
3344 if not branches:
3345 print('No local branches found.')
3346 return 0
3347
3348 # Create a dictionary of all local branches to the branches that are dependent
3349 # on it.
3350 tracked_to_dependents = collections.defaultdict(list)
3351 for b in branches.splitlines():
3352 tokens = b.split()
3353 if len(tokens) == 2:
3354 branch_name, tracked = tokens
3355 tracked_to_dependents[tracked].append(branch_name)
3356
vapiera7fbd5a2016-06-16 09:17:49 -07003357 print()
3358 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003359 dependents = []
3360 def traverse_dependents_preorder(branch, padding=''):
3361 dependents_to_process = tracked_to_dependents.get(branch, [])
3362 padding += ' '
3363 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003364 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003365 dependents.append(dependent)
3366 traverse_dependents_preorder(dependent, padding)
3367 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003368 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003369
3370 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003371 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003372 return 0
3373
vapiera7fbd5a2016-06-16 09:17:49 -07003374 print('This command will checkout all dependent branches and run '
3375 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003376 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3377
andybons@chromium.org962f9462016-02-03 20:00:42 +00003378 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003379 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003380 args.extend(['-t', 'Updated patchset dependency'])
3381
rmistry@google.com2dd99862015-06-22 12:22:18 +00003382 # Record all dependents that failed to upload.
3383 failures = {}
3384 # Go through all dependents, checkout the branch and upload.
3385 try:
3386 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003387 print()
3388 print('--------------------------------------')
3389 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003390 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003391 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003392 try:
3393 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003394 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003395 failures[dependent_branch] = 1
3396 except: # pylint: disable=W0702
3397 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003398 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003399 finally:
3400 # Swap back to the original root branch.
3401 RunGit(['checkout', '-q', root_branch])
3402
vapiera7fbd5a2016-06-16 09:17:49 -07003403 print()
3404 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003405 for dependent_branch in dependents:
3406 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003407 print(' %s : %s' % (dependent_branch, upload_status))
3408 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003409
3410 return 0
3411
3412
kmarshall3bff56b2016-06-06 18:31:47 -07003413def CMDarchive(parser, args):
3414 """Archives and deletes branches associated with closed changelists."""
3415 parser.add_option(
3416 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003417 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003418 parser.add_option(
3419 '-f', '--force', action='store_true',
3420 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003421 parser.add_option(
3422 '-d', '--dry-run', action='store_true',
3423 help='Skip the branch tagging and removal steps.')
3424 parser.add_option(
3425 '-t', '--notags', action='store_true',
3426 help='Do not tag archived branches. '
3427 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003428
3429 auth.add_auth_options(parser)
3430 options, args = parser.parse_args(args)
3431 if args:
3432 parser.error('Unsupported args: %s' % ' '.join(args))
3433 auth_config = auth.extract_auth_config_from_options(options)
3434
3435 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3436 if not branches:
3437 return 0
3438
vapiera7fbd5a2016-06-16 09:17:49 -07003439 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003440 changes = [Changelist(branchref=b, auth_config=auth_config)
3441 for b in branches.splitlines()]
3442 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3443 statuses = get_cl_statuses(changes,
3444 fine_grained=True,
3445 max_processes=options.maxjobs)
3446 proposal = [(cl.GetBranch(),
3447 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3448 for cl, status in statuses
3449 if status == 'closed']
3450 proposal.sort()
3451
3452 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003453 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003454 return 0
3455
3456 current_branch = GetCurrentBranch()
3457
vapiera7fbd5a2016-06-16 09:17:49 -07003458 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003459 if options.notags:
3460 for next_item in proposal:
3461 print(' ' + next_item[0])
3462 else:
3463 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3464 for next_item in proposal:
3465 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003466
kmarshall9249e012016-08-23 12:02:16 -07003467 # Quit now on precondition failure or if instructed by the user, either
3468 # via an interactive prompt or by command line flags.
3469 if options.dry_run:
3470 print('\nNo changes were made (dry run).\n')
3471 return 0
3472 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003473 print('You are currently on a branch \'%s\' which is associated with a '
3474 'closed codereview issue, so archive cannot proceed. Please '
3475 'checkout another branch and run this command again.' %
3476 current_branch)
3477 return 1
kmarshall9249e012016-08-23 12:02:16 -07003478 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003479 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3480 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003481 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003482 return 1
3483
3484 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003485 if not options.notags:
3486 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003487 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003488
vapiera7fbd5a2016-06-16 09:17:49 -07003489 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003490
3491 return 0
3492
3493
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003494def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003495 """Show status of changelists.
3496
3497 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003498 - Red not sent for review or broken
3499 - Blue waiting for review
3500 - Yellow waiting for you to reply to review
3501 - Green LGTM'ed
3502 - Magenta in the commit queue
3503 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003504
3505 Also see 'git cl comments'.
3506 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003507 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003508 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003509 parser.add_option('-f', '--fast', action='store_true',
3510 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003511 parser.add_option(
3512 '-j', '--maxjobs', action='store', type=int,
3513 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003514
3515 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003516 _add_codereview_issue_select_options(
3517 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003518 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003519 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003520 if args:
3521 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003522 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523
iannuccie53c9352016-08-17 14:40:40 -07003524 if options.issue is not None and not options.field:
3525 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003526
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003527 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003528 cl = Changelist(auth_config=auth_config, issue=options.issue,
3529 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003530 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003531 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532 elif options.field == 'id':
3533 issueid = cl.GetIssue()
3534 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003535 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003536 elif options.field == 'patch':
3537 patchset = cl.GetPatchset()
3538 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003539 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003540 elif options.field == 'status':
3541 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542 elif options.field == 'url':
3543 url = cl.GetIssueURL()
3544 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003546 return 0
3547
3548 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3549 if not branches:
3550 print('No local branch found.')
3551 return 0
3552
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003553 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003554 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003555 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003557 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003558 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003559 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003560
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003561 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003562 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3563 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3564 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003565 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003566 c, status = output.next()
3567 branch_statuses[c.GetBranch()] = status
3568 status = branch_statuses.pop(branch)
3569 url = cl.GetIssueURL()
3570 if url and (not status or status == 'error'):
3571 # The issue probably doesn't exist anymore.
3572 url += ' (broken)'
3573
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003574 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003575 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003576 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003577 color = ''
3578 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003579 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003580 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003581 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003582 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003583
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003584 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003585 print()
3586 print('Current branch:',)
3587 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003588 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003589 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003590 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003592 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003593 print('Issue description:')
3594 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003595 return 0
3596
3597
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003598def colorize_CMDstatus_doc():
3599 """To be called once in main() to add colors to git cl status help."""
3600 colors = [i for i in dir(Fore) if i[0].isupper()]
3601
3602 def colorize_line(line):
3603 for color in colors:
3604 if color in line.upper():
3605 # Extract whitespaces first and the leading '-'.
3606 indent = len(line) - len(line.lstrip(' ')) + 1
3607 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3608 return line
3609
3610 lines = CMDstatus.__doc__.splitlines()
3611 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3612
3613
phajdan.jre328cf92016-08-22 04:12:17 -07003614def write_json(path, contents):
3615 with open(path, 'w') as f:
3616 json.dump(contents, f)
3617
3618
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003619@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003621 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003622
3623 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003624 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003625 parser.add_option('-r', '--reverse', action='store_true',
3626 help='Lookup the branch(es) for the specified issues. If '
3627 'no issues are specified, all branches with mapped '
3628 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003629 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003630 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003631 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003632 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003633
dnj@chromium.org406c4402015-03-03 17:22:28 +00003634 if options.reverse:
3635 branches = RunGit(['for-each-ref', 'refs/heads',
3636 '--format=%(refname:short)']).splitlines()
3637
3638 # Reverse issue lookup.
3639 issue_branch_map = {}
3640 for branch in branches:
3641 cl = Changelist(branchref=branch)
3642 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3643 if not args:
3644 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003645 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003646 for issue in args:
3647 if not issue:
3648 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003649 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003650 print('Branch for issue number %s: %s' % (
3651 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003652 if options.json:
3653 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003654 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003655 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003656 if len(args) > 0:
3657 try:
3658 issue = int(args[0])
3659 except ValueError:
3660 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003661 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003662 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003663 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003664 if options.json:
3665 write_json(options.json, {
3666 'issue': cl.GetIssue(),
3667 'issue_url': cl.GetIssueURL(),
3668 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 return 0
3670
3671
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003672def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003673 """Shows or posts review comments for any changelist."""
3674 parser.add_option('-a', '--add-comment', dest='comment',
3675 help='comment to add to an issue')
3676 parser.add_option('-i', dest='issue',
3677 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003678 parser.add_option('-j', '--json-file',
3679 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003680 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003681 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003682 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003683
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003684 issue = None
3685 if options.issue:
3686 try:
3687 issue = int(options.issue)
3688 except ValueError:
3689 DieWithError('A review issue id is expected to be a number')
3690
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003691 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003692
3693 if options.comment:
3694 cl.AddComment(options.comment)
3695 return 0
3696
3697 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003698 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003699 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003700 summary.append({
3701 'date': message['date'],
3702 'lgtm': False,
3703 'message': message['text'],
3704 'not_lgtm': False,
3705 'sender': message['sender'],
3706 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003707 if message['disapproval']:
3708 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003709 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003710 elif message['approval']:
3711 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003712 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003713 elif message['sender'] == data['owner_email']:
3714 color = Fore.MAGENTA
3715 else:
3716 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003718 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003719 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003720 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003722 if options.json_file:
3723 with open(options.json_file, 'wb') as f:
3724 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003725 return 0
3726
3727
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003728@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003729def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003730 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003731 parser.add_option('-d', '--display', action='store_true',
3732 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003733 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003734 help='New description to set for this issue (- for stdin, '
3735 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003736 parser.add_option('-f', '--force', action='store_true',
3737 help='Delete any unpublished Gerrit edits for this issue '
3738 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003739
3740 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003741 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003742 options, args = parser.parse_args(args)
3743 _process_codereview_select_options(parser, options)
3744
3745 target_issue = None
3746 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003747 target_issue = ParseIssueNumberArgument(args[0])
3748 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003749 parser.print_help()
3750 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003751
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003752 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003753
martiniss6eda05f2016-06-30 10:18:35 -07003754 kwargs = {
3755 'auth_config': auth_config,
3756 'codereview': options.forced_codereview,
3757 }
3758 if target_issue:
3759 kwargs['issue'] = target_issue.issue
3760 if options.forced_codereview == 'rietveld':
3761 kwargs['rietveld_server'] = target_issue.hostname
3762
3763 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003764
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003765 if not cl.GetIssue():
3766 DieWithError('This branch has no associated changelist.')
3767 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003768
smut@google.com34fb6b12015-07-13 20:03:26 +00003769 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003770 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003771 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003772
3773 if options.new_description:
3774 text = options.new_description
3775 if text == '-':
3776 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003777 elif text == '+':
3778 base_branch = cl.GetCommonAncestorWithUpstream()
3779 change = cl.GetChange(base_branch, None, local_description=True)
3780 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003781
3782 description.set_description(text)
3783 else:
3784 description.prompt()
3785
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003786 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003787 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003788 return 0
3789
3790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791def CreateDescriptionFromLog(args):
3792 """Pulls out the commit log to use as a base for the CL description."""
3793 log_args = []
3794 if len(args) == 1 and not args[0].endswith('.'):
3795 log_args = [args[0] + '..']
3796 elif len(args) == 1 and args[0].endswith('...'):
3797 log_args = [args[0][:-1]]
3798 elif len(args) == 2:
3799 log_args = [args[0] + '..' + args[1]]
3800 else:
3801 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003802 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803
3804
thestig@chromium.org44202a22014-03-11 19:22:18 +00003805def CMDlint(parser, args):
3806 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003807 parser.add_option('--filter', action='append', metavar='-x,+y',
3808 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003809 auth.add_auth_options(parser)
3810 options, args = parser.parse_args(args)
3811 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003812
3813 # Access to a protected member _XX of a client class
3814 # pylint: disable=W0212
3815 try:
3816 import cpplint
3817 import cpplint_chromium
3818 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003819 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003820 return 1
3821
3822 # Change the current working directory before calling lint so that it
3823 # shows the correct base.
3824 previous_cwd = os.getcwd()
3825 os.chdir(settings.GetRoot())
3826 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003827 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003828 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3829 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003830 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003831 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003832 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003833
3834 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003835 command = args + files
3836 if options.filter:
3837 command = ['--filter=' + ','.join(options.filter)] + command
3838 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003839
3840 white_regex = re.compile(settings.GetLintRegex())
3841 black_regex = re.compile(settings.GetLintIgnoreRegex())
3842 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3843 for filename in filenames:
3844 if white_regex.match(filename):
3845 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003847 else:
3848 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3849 extra_check_functions)
3850 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003851 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003852 finally:
3853 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003854 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003855 if cpplint._cpplint_state.error_count != 0:
3856 return 1
3857 return 0
3858
3859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003861 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003862 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003863 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003864 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003865 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003866 auth.add_auth_options(parser)
3867 options, args = parser.parse_args(args)
3868 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869
sbc@chromium.org71437c02015-04-09 19:29:40 +00003870 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003871 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003872 return 1
3873
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003874 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003875 if args:
3876 base_branch = args[0]
3877 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003878 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003879 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003880
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003881 cl.RunHook(
3882 committing=not options.upload,
3883 may_prompt=False,
3884 verbose=options.verbose,
3885 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003886 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003887
3888
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003889def GenerateGerritChangeId(message):
3890 """Returns Ixxxxxx...xxx change id.
3891
3892 Works the same way as
3893 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3894 but can be called on demand on all platforms.
3895
3896 The basic idea is to generate git hash of a state of the tree, original commit
3897 message, author/committer info and timestamps.
3898 """
3899 lines = []
3900 tree_hash = RunGitSilent(['write-tree'])
3901 lines.append('tree %s' % tree_hash.strip())
3902 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3903 if code == 0:
3904 lines.append('parent %s' % parent.strip())
3905 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3906 lines.append('author %s' % author.strip())
3907 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3908 lines.append('committer %s' % committer.strip())
3909 lines.append('')
3910 # Note: Gerrit's commit-hook actually cleans message of some lines and
3911 # whitespace. This code is not doing this, but it clearly won't decrease
3912 # entropy.
3913 lines.append(message)
3914 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3915 stdin='\n'.join(lines))
3916 return 'I%s' % change_hash.strip()
3917
3918
wittman@chromium.org455dc922015-01-26 20:15:50 +00003919def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3920 """Computes the remote branch ref to use for the CL.
3921
3922 Args:
3923 remote (str): The git remote for the CL.
3924 remote_branch (str): The git remote branch for the CL.
3925 target_branch (str): The target branch specified by the user.
3926 pending_prefix (str): The pending prefix from the settings.
3927 """
3928 if not (remote and remote_branch):
3929 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003930
wittman@chromium.org455dc922015-01-26 20:15:50 +00003931 if target_branch:
3932 # Cannonicalize branch references to the equivalent local full symbolic
3933 # refs, which are then translated into the remote full symbolic refs
3934 # below.
3935 if '/' not in target_branch:
3936 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3937 else:
3938 prefix_replacements = (
3939 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3940 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3941 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3942 )
3943 match = None
3944 for regex, replacement in prefix_replacements:
3945 match = re.search(regex, target_branch)
3946 if match:
3947 remote_branch = target_branch.replace(match.group(0), replacement)
3948 break
3949 if not match:
3950 # This is a branch path but not one we recognize; use as-is.
3951 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003952 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3953 # Handle the refs that need to land in different refs.
3954 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003955
wittman@chromium.org455dc922015-01-26 20:15:50 +00003956 # Create the true path to the remote branch.
3957 # Does the following translation:
3958 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3959 # * refs/remotes/origin/master -> refs/heads/master
3960 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3961 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3962 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3963 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3964 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3965 'refs/heads/')
3966 elif remote_branch.startswith('refs/remotes/branch-heads'):
3967 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3968 # If a pending prefix exists then replace refs/ with it.
3969 if pending_prefix:
3970 remote_branch = remote_branch.replace('refs/', pending_prefix)
3971 return remote_branch
3972
3973
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003974def cleanup_list(l):
3975 """Fixes a list so that comma separated items are put as individual items.
3976
3977 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3978 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3979 """
3980 items = sum((i.split(',') for i in l), [])
3981 stripped_items = (i.strip() for i in items)
3982 return sorted(filter(None, stripped_items))
3983
3984
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003985@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003986def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003987 """Uploads the current changelist to codereview.
3988
3989 Can skip dependency patchset uploads for a branch by running:
3990 git config branch.branch_name.skip-deps-uploads True
3991 To unset run:
3992 git config --unset branch.branch_name.skip-deps-uploads
3993 Can also set the above globally by using the --global flag.
3994 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003995 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3996 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003997 parser.add_option('--bypass-watchlists', action='store_true',
3998 dest='bypass_watchlists',
3999 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004000 parser.add_option('-f', action='store_true', dest='force',
4001 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004002 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004003 parser.add_option('-b', '--bug',
4004 help='pre-populate the bug number(s) for this issue. '
4005 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004006 parser.add_option('--message-file', dest='message_file',
4007 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004008 parser.add_option('-t', dest='title',
4009 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004010 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004011 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004012 help='reviewer email addresses')
4013 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004014 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004015 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004016 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004017 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004018 parser.add_option('--emulate_svn_auto_props',
4019 '--emulate-svn-auto-props',
4020 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004021 dest="emulate_svn_auto_props",
4022 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004023 parser.add_option('-c', '--use-commit-queue', action='store_true',
4024 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004025 parser.add_option('--private', action='store_true',
4026 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004027 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004028 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004029 metavar='TARGET',
4030 help='Apply CL to remote ref TARGET. ' +
4031 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004032 parser.add_option('--squash', action='store_true',
4033 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004034 parser.add_option('--no-squash', action='store_true',
4035 help='Don\'t squash multiple commits into one ' +
4036 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004037 parser.add_option('--topic', default=None,
4038 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004039 parser.add_option('--email', default=None,
4040 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004041 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4042 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004043 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4044 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004045 help='Send the patchset to do a CQ dry run right after '
4046 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004047 parser.add_option('--dependencies', action='store_true',
4048 help='Uploads CLs of all the local branches that depend on '
4049 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004050
rmistry@google.com2dd99862015-06-22 12:22:18 +00004051 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004052 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004053 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004054 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004055 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004056 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004057 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004058
sbc@chromium.org71437c02015-04-09 19:29:40 +00004059 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004060 return 1
4061
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004062 options.reviewers = cleanup_list(options.reviewers)
4063 options.cc = cleanup_list(options.cc)
4064
tandriib80458a2016-06-23 12:20:07 -07004065 if options.message_file:
4066 if options.message:
4067 parser.error('only one of --message and --message-file allowed.')
4068 options.message = gclient_utils.FileRead(options.message_file)
4069 options.message_file = None
4070
tandrii4d0545a2016-07-06 03:56:49 -07004071 if options.cq_dry_run and options.use_commit_queue:
4072 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4073
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004074 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4075 settings.GetIsGerrit()
4076
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004077 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004078 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004079
4080
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004081def IsSubmoduleMergeCommit(ref):
4082 # When submodules are added to the repo, we expect there to be a single
4083 # non-git-svn merge commit at remote HEAD with a signature comment.
4084 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004085 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004086 return RunGit(cmd) != ''
4087
4088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004089def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004090 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004091
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004092 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4093 upstream and closes the issue automatically and atomically.
4094
4095 Otherwise (in case of Rietveld):
4096 Squashes branch into a single commit.
4097 Updates changelog with metadata (e.g. pointer to review).
4098 Pushes/dcommits the code upstream.
4099 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100 """
4101 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4102 help='bypass upload presubmit hook')
4103 parser.add_option('-m', dest='message',
4104 help="override review description")
4105 parser.add_option('-f', action='store_true', dest='force',
4106 help="force yes to questions (don't prompt)")
4107 parser.add_option('-c', dest='contributor',
4108 help="external contributor for patch (appended to " +
4109 "description and used as author for git). Should be " +
4110 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004111 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004112 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004113 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004114 auth_config = auth.extract_auth_config_from_options(options)
4115
4116 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004117
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004118 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4119 if cl.IsGerrit():
4120 if options.message:
4121 # This could be implemented, but it requires sending a new patch to
4122 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4123 # Besides, Gerrit has the ability to change the commit message on submit
4124 # automatically, thus there is no need to support this option (so far?).
4125 parser.error('-m MESSAGE option is not supported for Gerrit.')
4126 if options.contributor:
4127 parser.error(
4128 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4129 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4130 'the contributor\'s "name <email>". If you can\'t upload such a '
4131 'commit for review, contact your repository admin and request'
4132 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004133 if not cl.GetIssue():
4134 DieWithError('You must upload the issue first to Gerrit.\n'
4135 ' If you would rather have `git cl land` upload '
4136 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004137 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4138 options.verbose)
4139
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004140 current = cl.GetBranch()
4141 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4142 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print()
4144 print('Attempting to push branch %r into another local branch!' % current)
4145 print()
4146 print('Either reparent this branch on top of origin/master:')
4147 print(' git reparent-branch --root')
4148 print()
4149 print('OR run `git rebase-update` if you think the parent branch is ')
4150 print('already committed.')
4151 print()
4152 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004153 return 1
4154
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004155 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004156 # Default to merging against our best guess of the upstream branch.
4157 args = [cl.GetUpstreamBranch()]
4158
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004159 if options.contributor:
4160 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004162 return 1
4163
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004164 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004165 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166
sbc@chromium.org71437c02015-04-09 19:29:40 +00004167 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168 return 1
4169
4170 # This rev-list syntax means "show all commits not in my branch that
4171 # are in base_branch".
4172 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4173 base_branch]).splitlines()
4174 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print('Base branch "%s" has %d commits '
4176 'not in this branch.' % (base_branch, len(upstream_commits)))
4177 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178 return 1
4179
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004180 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004181 svn_head = None
4182 if cmd == 'dcommit' or base_has_submodules:
4183 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4184 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004186 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004187 # If the base_head is a submodule merge commit, the first parent of the
4188 # base_head should be a git-svn commit, which is what we're interested in.
4189 base_svn_head = base_branch
4190 if base_has_submodules:
4191 base_svn_head += '^1'
4192
4193 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004195 print('This branch has %d additional commits not upstreamed yet.'
4196 % len(extra_commits.splitlines()))
4197 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4198 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004199 return 1
4200
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004201 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004202 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004203 author = None
4204 if options.contributor:
4205 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004206 hook_results = cl.RunHook(
4207 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004208 may_prompt=not options.force,
4209 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004210 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004211 if not hook_results.should_continue():
4212 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004214 # Check the tree status if the tree status URL is set.
4215 status = GetTreeStatus()
4216 if 'closed' == status:
4217 print('The tree is closed. Please wait for it to reopen. Use '
4218 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4219 return 1
4220 elif 'unknown' == status:
4221 print('Unable to determine tree status. Please verify manually and '
4222 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4223 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004225 change_desc = ChangeDescription(options.message)
4226 if not change_desc.description and cl.GetIssue():
4227 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004229 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004230 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004231 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004232 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004233 print('No description set.')
4234 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004235 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004237 # Keep a separate copy for the commit message, because the commit message
4238 # contains the link to the Rietveld issue, while the Rietveld message contains
4239 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004240 # Keep a separate copy for the commit message.
4241 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004242 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004243
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004244 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004245 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004246 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004247 # after it. Add a period on a new line to circumvent this. Also add a space
4248 # before the period to make sure that Gitiles continues to correctly resolve
4249 # the URL.
4250 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004252 commit_desc.append_footer('Patch from %s.' % options.contributor)
4253
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004254 print('Description:')
4255 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004256
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004257 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004259 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004260
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004261 # We want to squash all this branch's commits into one commit with the proper
4262 # description. We do this by doing a "reset --soft" to the base branch (which
4263 # keeps the working copy the same), then dcommitting that. If origin/master
4264 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4265 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004267 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4268 # Delete the branches if they exist.
4269 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4270 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4271 result = RunGitWithCode(showref_cmd)
4272 if result[0] == 0:
4273 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004274
4275 # We might be in a directory that's present in this branch but not in the
4276 # trunk. Move up to the top of the tree so that git commands that expect a
4277 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004278 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004279 if rel_base_path:
4280 os.chdir(rel_base_path)
4281
4282 # Stuff our change into the merge branch.
4283 # We wrap in a try...finally block so if anything goes wrong,
4284 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004285 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004286 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004287 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004288 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004289 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004290 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004291 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004292 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004293 RunGit(
4294 [
4295 'commit', '--author', options.contributor,
4296 '-m', commit_desc.description,
4297 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004299 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004300 if base_has_submodules:
4301 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4302 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4303 RunGit(['checkout', CHERRY_PICK_BRANCH])
4304 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004305 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004306 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004307 mirror = settings.GetGitMirror(remote)
4308 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004309 pending_prefix = settings.GetPendingRefPrefix()
4310 if not pending_prefix or branch.startswith(pending_prefix):
4311 # If not using refs/pending/heads/* at all, or target ref is already set
4312 # to pending, then push to the target ref directly.
4313 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004314 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004315 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004316 else:
4317 # Cherry-pick the change on top of pending ref and then push it.
4318 assert branch.startswith('refs/'), branch
4319 assert pending_prefix[-1] == '/', pending_prefix
4320 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004321 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004322 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004323 if retcode == 0:
4324 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004325 else:
4326 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004327 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004328 'svn', 'dcommit',
4329 '-C%s' % options.similarity,
4330 '--no-rebase', '--rmdir',
4331 ]
4332 if settings.GetForceHttpsCommitUrl():
4333 # Allow forcing https commit URLs for some projects that don't allow
4334 # committing to http URLs (like Google Code).
4335 remote_url = cl.GetGitSvnRemoteUrl()
4336 if urlparse.urlparse(remote_url).scheme == 'http':
4337 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004338 cmd_args.append('--commit-url=%s' % remote_url)
4339 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004340 if 'Committed r' in output:
4341 revision = re.match(
4342 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4343 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004344 finally:
4345 # And then swap back to the original branch and clean up.
4346 RunGit(['checkout', '-q', cl.GetBranch()])
4347 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004348 if base_has_submodules:
4349 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004350
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004351 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004352 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004353 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004354
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004355 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004356 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004357 try:
4358 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4359 # We set pushed_to_pending to False, since it made it all the way to the
4360 # real ref.
4361 pushed_to_pending = False
4362 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004363 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004364
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004365 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004366 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004367 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004368 if not to_pending:
4369 if viewvc_url and revision:
4370 change_desc.append_footer(
4371 'Committed: %s%s' % (viewvc_url, revision))
4372 elif revision:
4373 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004374 print('Closing issue '
4375 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004376 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004377 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004378 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004379 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004380 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004381 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004382 if options.bypass_hooks:
4383 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4384 else:
4385 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004386 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004387
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004388 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004389 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004390 print('The commit is in the pending queue (%s).' % pending_ref)
4391 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4392 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004393
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004394 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4395 if os.path.isfile(hook):
4396 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004397
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004398 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399
4400
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004401def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004402 print()
4403 print('Waiting for commit to be landed on %s...' % real_ref)
4404 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004405 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4406 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004407 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004408
4409 loop = 0
4410 while True:
4411 sys.stdout.write('fetching (%d)... \r' % loop)
4412 sys.stdout.flush()
4413 loop += 1
4414
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004415 if mirror:
4416 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004417 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4418 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4419 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4420 for commit in commits.splitlines():
4421 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004422 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004423 return commit
4424
4425 current_rev = to_rev
4426
4427
tandriibf429402016-09-14 07:09:12 -07004428def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004429 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4430
4431 Returns:
4432 (retcode of last operation, output log of last operation).
4433 """
4434 assert pending_ref.startswith('refs/'), pending_ref
4435 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4436 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4437 code = 0
4438 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004439 max_attempts = 3
4440 attempts_left = max_attempts
4441 while attempts_left:
4442 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004443 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004444 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004445
4446 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004447 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004448 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004449 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004450 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004451 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004452 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004453 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004454 continue
4455
4456 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004457 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004458 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004459 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004460 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4462 'the following files have merge conflicts:' % pending_ref)
4463 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4464 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004465 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004466 return code, out
4467
4468 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004470 code, out = RunGitWithCode(
4471 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4472 if code == 0:
4473 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004474 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004475 return code, out
4476
vapiera7fbd5a2016-06-16 09:17:49 -07004477 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004478 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004479 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004480 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004481 print('Fatal push error. Make sure your .netrc credentials and git '
4482 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004483 return code, out
4484
vapiera7fbd5a2016-06-16 09:17:49 -07004485 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004486 return code, out
4487
4488
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004489def IsFatalPushFailure(push_stdout):
4490 """True if retrying push won't help."""
4491 return '(prohibited by Gerrit)' in push_stdout
4492
4493
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004494@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004495def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004496 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004497 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004498 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004499 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004500 message = """This repository appears to be a git-svn mirror, but we
4501don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004502 else:
4503 message = """This doesn't appear to be an SVN repository.
4504If your project has a true, writeable git repository, you probably want to run
4505'git cl land' instead.
4506If your project has a git mirror of an upstream SVN master, you probably need
4507to run 'git svn init'.
4508
4509Using the wrong command might cause your commit to appear to succeed, and the
4510review to be closed, without actually landing upstream. If you choose to
4511proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004512 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004513 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004514 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4515 'Please let us know of this project you are committing to:'
4516 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004517 return SendUpstream(parser, args, 'dcommit')
4518
4519
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004520@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004521def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004522 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004523 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004524 print('This appears to be an SVN repository.')
4525 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004526 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004527 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004528 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529
4530
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004531@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004533 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004534 parser.add_option('-b', dest='newbranch',
4535 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004536 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004538 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4539 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004540 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004541 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004542 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004543 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004544 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004545 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004546
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004547
4548 group = optparse.OptionGroup(
4549 parser,
4550 'Options for continuing work on the current issue uploaded from a '
4551 'different clone (e.g. different machine). Must be used independently '
4552 'from the other options. No issue number should be specified, and the '
4553 'branch must have an issue number associated with it')
4554 group.add_option('--reapply', action='store_true', dest='reapply',
4555 help='Reset the branch and reapply the issue.\n'
4556 'CAUTION: This will undo any local changes in this '
4557 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004558
4559 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004560 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004561 parser.add_option_group(group)
4562
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004563 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004564 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004565 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004566 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004567 auth_config = auth.extract_auth_config_from_options(options)
4568
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004569
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004570 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004571 if options.newbranch:
4572 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004573 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004574 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004575
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004576 cl = Changelist(auth_config=auth_config,
4577 codereview=options.forced_codereview)
4578 if not cl.GetIssue():
4579 parser.error('current branch must have an associated issue')
4580
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004581 upstream = cl.GetUpstreamBranch()
4582 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004583 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004584
4585 RunGit(['reset', '--hard', upstream])
4586 if options.pull:
4587 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004588
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004589 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4590 options.directory)
4591
4592 if len(args) != 1 or not args[0]:
4593 parser.error('Must specify issue number or url')
4594
4595 # We don't want uncommitted changes mixed up with the patch.
4596 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004597 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004598
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004599 if options.newbranch:
4600 if options.force:
4601 RunGit(['branch', '-D', options.newbranch],
4602 stderr=subprocess2.PIPE, error_ok=True)
4603 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004604 elif not GetCurrentBranch():
4605 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004606
4607 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4608
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004609 if cl.IsGerrit():
4610 if options.reject:
4611 parser.error('--reject is not supported with Gerrit codereview.')
4612 if options.nocommit:
4613 parser.error('--nocommit is not supported with Gerrit codereview.')
4614 if options.directory:
4615 parser.error('--directory is not supported with Gerrit codereview.')
4616
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004617 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004618 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004619
4620
4621def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004622 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 # Provide a wrapper for git svn rebase to help avoid accidental
4624 # git svn dcommit.
4625 # It's the only command that doesn't use parser at all since we just defer
4626 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004627
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004628 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004629
4630
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004631def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004632 """Fetches the tree status and returns either 'open', 'closed',
4633 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004634 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004635 if url:
4636 status = urllib2.urlopen(url).read().lower()
4637 if status.find('closed') != -1 or status == '0':
4638 return 'closed'
4639 elif status.find('open') != -1 or status == '1':
4640 return 'open'
4641 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642 return 'unset'
4643
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004645def GetTreeStatusReason():
4646 """Fetches the tree status from a json url and returns the message
4647 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004648 url = settings.GetTreeStatusUrl()
4649 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650 connection = urllib2.urlopen(json_url)
4651 status = json.loads(connection.read())
4652 connection.close()
4653 return status['message']
4654
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004655
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004656def GetBuilderMaster(bot_list):
4657 """For a given builder, fetch the master from AE if available."""
4658 map_url = 'https://builders-map.appspot.com/'
4659 try:
4660 master_map = json.load(urllib2.urlopen(map_url))
4661 except urllib2.URLError as e:
4662 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4663 (map_url, e))
4664 except ValueError as e:
4665 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4666 if not master_map:
4667 return None, 'Failed to build master map.'
4668
4669 result_master = ''
4670 for bot in bot_list:
4671 builder = bot.split(':', 1)[0]
4672 master_list = master_map.get(builder, [])
4673 if not master_list:
4674 return None, ('No matching master for builder %s.' % builder)
4675 elif len(master_list) > 1:
4676 return None, ('The builder name %s exists in multiple masters %s.' %
4677 (builder, master_list))
4678 else:
4679 cur_master = master_list[0]
4680 if not result_master:
4681 result_master = cur_master
4682 elif result_master != cur_master:
4683 return None, 'The builders do not belong to the same master.'
4684 return result_master, None
4685
4686
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004688 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004689 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690 status = GetTreeStatus()
4691 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004692 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693 return 2
4694
vapiera7fbd5a2016-06-16 09:17:49 -07004695 print('The tree is %s' % status)
4696 print()
4697 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698 if status != 'open':
4699 return 1
4700 return 0
4701
4702
maruel@chromium.org15192402012-09-06 12:38:29 +00004703def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004704 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4705 """
tandrii1838bad2016-10-06 00:10:52 -07004706 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004707 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004708 '-b', '--bot', action='append',
4709 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4710 'times to specify multiple builders. ex: '
4711 '"-b win_rel -b win_layout". See '
4712 'the try server waterfall for the builders name and the tests '
4713 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004714 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004715 '-m', '--master', default='',
4716 help=('Specify a try master where to run the tries.'))
tandriif7b29d42016-10-07 08:45:41 -07004717 # TODO(tandrii,nodir): add -B --bucket flag.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004718 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004719 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004720 help='Revision to use for the try job; default: the revision will '
4721 'be determined by the try recipe that builder runs, which usually '
4722 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004723 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004724 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004725 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004726 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004727 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004728 '--project',
4729 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004730 'in recipe to determine to which repository or directory to '
4731 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004732 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004733 '-p', '--property', dest='properties', action='append', default=[],
4734 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004735 'key2=value2 etc. The value will be treated as '
4736 'json if decodable, or as string otherwise. '
4737 'NOTE: using this may make your try job not usable for CQ, '
4738 'which will then schedule another try job with default properties')
4739 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004740 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004741 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004742 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004743 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4744 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004745 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004746 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004747 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004748 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004749
machenbach@chromium.org45453142015-09-15 08:45:22 +00004750 # Make sure that all properties are prop=value pairs.
4751 bad_params = [x for x in options.properties if '=' not in x]
4752 if bad_params:
4753 parser.error('Got properties with missing "=": %s' % bad_params)
4754
maruel@chromium.org15192402012-09-06 12:38:29 +00004755 if args:
4756 parser.error('Unknown arguments: %s' % args)
4757
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004758 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004759 if not cl.GetIssue():
4760 parser.error('Need to upload first')
4761
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004762 if cl.IsGerrit():
4763 parser.error(
4764 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4765 'If your project has Commit Queue, dry run is a workaround:\n'
4766 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004767
tandriie113dfd2016-10-11 10:20:12 -07004768 error_message = cl.CannotTriggerTryJobReason()
4769 if error_message:
4770 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004771
maruel@chromium.org15192402012-09-06 12:38:29 +00004772 if not options.name:
4773 options.name = cl.GetBranch()
4774
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004775 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004776 options.master, err_msg = GetBuilderMaster(options.bot)
4777 if err_msg:
4778 parser.error('Tryserver master cannot be found because: %s\n'
4779 'Please manually specify the tryserver master'
4780 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004781
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004782 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004783 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004784 if not options.bot:
4785 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004786
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004787 # Get try masters from PRESUBMIT.py files.
4788 masters = presubmit_support.DoGetTryMasters(
4789 change,
4790 change.LocalPaths(),
4791 settings.GetRoot(),
4792 None,
4793 None,
4794 options.verbose,
4795 sys.stdout)
4796 if masters:
4797 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004798
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004799 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4800 options.bot = presubmit_support.DoGetTrySlaves(
4801 change,
4802 change.LocalPaths(),
4803 settings.GetRoot(),
4804 None,
4805 None,
4806 options.verbose,
4807 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004808
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004809 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004810 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004811
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004812 builders_and_tests = {}
4813 # TODO(machenbach): The old style command-line options don't support
4814 # multiple try masters yet.
4815 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4816 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4817
4818 for bot in old_style:
4819 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004820 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004821 elif ',' in bot:
4822 parser.error('Specify one bot per --bot flag')
4823 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004824 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004825
4826 for bot, tests in new_style:
4827 builders_and_tests.setdefault(bot, []).extend(tests)
4828
4829 # Return a master map with one master to be backwards compatible. The
4830 # master name defaults to an empty string, which will cause the master
4831 # not to be set on rietveld (deprecated).
4832 return {options.master: builders_and_tests}
4833
4834 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004835 if not masters:
4836 # Default to triggering Dry Run (see http://crbug.com/625697).
4837 if options.verbose:
4838 print('git cl try with no bots now defaults to CQ Dry Run.')
4839 try:
4840 cl.SetCQState(_CQState.DRY_RUN)
4841 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4842 return 0
4843 except KeyboardInterrupt:
4844 raise
4845 except:
4846 print('WARNING: failed to trigger CQ Dry Run.\n'
4847 'Either:\n'
4848 ' * your project has no CQ\n'
4849 ' * you don\'t have permission to trigger Dry Run\n'
4850 ' * bug in this code (see stack trace below).\n'
4851 'Consider specifying which bots to trigger manually '
4852 'or asking your project owners for permissions '
4853 'or contacting Chrome Infrastructure team at '
4854 'https://www.chromium.org/infra\n\n')
4855 # Still raise exception so that stack trace is printed.
4856 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004857
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004858 for builders in masters.itervalues():
4859 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004860 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004861 'of bot requires an initial job from a parent (usually a builder). '
4862 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004863 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004864 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004865
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004866 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004867 if patchset != cl.GetPatchset():
4868 print('Warning: Codereview server has newer patchsets (%s) than most '
4869 'recent upload from local checkout (%s). Did a previous upload '
4870 'fail?\n'
4871 'By default, git cl try uses the latest patchset from '
4872 'codereview, continuing to use patchset %s.\n' %
4873 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004874 try:
tandriide281ae2016-10-12 06:02:30 -07004875 _trigger_try_jobs(auth_config, cl, masters, options, 'git_cl_try',
4876 patchset)
tandrii568043b2016-10-11 07:49:18 -07004877 except BuildbucketResponseException as ex:
4878 print('ERROR: %s' % ex)
4879 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004880 return 0
4881
4882
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004883def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004884 """Prints info about try jobs associated with current CL."""
4885 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004886 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004887 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004888 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004889 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004890 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004891 '--color', action='store_true', default=setup_color.IS_TTY,
4892 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004893 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004894 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4895 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004896 group.add_option(
4897 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004898 parser.add_option_group(group)
4899 auth.add_auth_options(parser)
4900 options, args = parser.parse_args(args)
4901 if args:
4902 parser.error('Unrecognized args: %s' % ' '.join(args))
4903
4904 auth_config = auth.extract_auth_config_from_options(options)
4905 cl = Changelist(auth_config=auth_config)
4906 if not cl.GetIssue():
4907 parser.error('Need to upload first')
4908
tandrii221ab252016-10-06 08:12:04 -07004909 patchset = options.patchset
4910 if not patchset:
4911 patchset = cl.GetMostRecentPatchset()
4912 if not patchset:
4913 parser.error('Codereview doesn\'t know about issue %s. '
4914 'No access to issue or wrong issue number?\n'
4915 'Either upload first, or pass --patchset explicitely' %
4916 cl.GetIssue())
4917
4918 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004919 print('Warning: Codereview server has newer patchsets (%s) than most '
4920 'recent upload from local checkout (%s). Did a previous upload '
4921 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004922 'By default, git cl try-results uses the latest patchset from '
4923 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004924 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004925 try:
tandrii221ab252016-10-06 08:12:04 -07004926 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004927 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004928 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004929 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004930 if options.json:
4931 write_try_results_json(options.json, jobs)
4932 else:
4933 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004934 return 0
4935
4936
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004937@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004938def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004939 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004940 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004941 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004942 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004944 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004945 if args:
4946 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004947 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004948 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004949 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004950 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004951
4952 # Clear configured merge-base, if there is one.
4953 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004954 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004955 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004956 return 0
4957
4958
thestig@chromium.org00858c82013-12-02 23:08:03 +00004959def CMDweb(parser, args):
4960 """Opens the current CL in the web browser."""
4961 _, args = parser.parse_args(args)
4962 if args:
4963 parser.error('Unrecognized args: %s' % ' '.join(args))
4964
4965 issue_url = Changelist().GetIssueURL()
4966 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004967 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004968 return 1
4969
4970 webbrowser.open(issue_url)
4971 return 0
4972
4973
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004974def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004975 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004976 parser.add_option('-d', '--dry-run', action='store_true',
4977 help='trigger in dry run mode')
4978 parser.add_option('-c', '--clear', action='store_true',
4979 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004980 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004981 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004982 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004983 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004984 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004985 if args:
4986 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004987 if options.dry_run and options.clear:
4988 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4989
iannuccie53c9352016-08-17 14:40:40 -07004990 cl = Changelist(auth_config=auth_config, issue=options.issue,
4991 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004992 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004993 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004994 elif options.dry_run:
4995 state = _CQState.DRY_RUN
4996 else:
4997 state = _CQState.COMMIT
4998 if not cl.GetIssue():
4999 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005000 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005001 return 0
5002
5003
groby@chromium.org411034a2013-02-26 15:12:01 +00005004def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005005 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005006 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005007 auth.add_auth_options(parser)
5008 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005009 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005010 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005011 if args:
5012 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005013 cl = Changelist(auth_config=auth_config, issue=options.issue,
5014 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005015 # Ensure there actually is an issue to close.
5016 cl.GetDescription()
5017 cl.CloseIssue()
5018 return 0
5019
5020
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005021def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005022 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005023 parser.add_option(
5024 '--stat',
5025 action='store_true',
5026 dest='stat',
5027 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005028 auth.add_auth_options(parser)
5029 options, args = parser.parse_args(args)
5030 auth_config = auth.extract_auth_config_from_options(options)
5031 if args:
5032 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005033
5034 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005035 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005036 # Staged changes would be committed along with the patch from last
5037 # upload, hence counted toward the "last upload" side in the final
5038 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005039 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005040 return 1
5041
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005042 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005043 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005044 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005045 if not issue:
5046 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005047 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005048 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005049
5050 # Create a new branch based on the merge-base
5051 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005052 # Clear cached branch in cl object, to avoid overwriting original CL branch
5053 # properties.
5054 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005055 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005056 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005057 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005058 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005059 return rtn
5060
wychen@chromium.org06928532015-02-03 02:11:29 +00005061 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005062 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005063 cmd = ['git', 'diff']
5064 if options.stat:
5065 cmd.append('--stat')
5066 cmd.extend([TMP_BRANCH, branch, '--'])
5067 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005068 finally:
5069 RunGit(['checkout', '-q', branch])
5070 RunGit(['branch', '-D', TMP_BRANCH])
5071
5072 return 0
5073
5074
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005075def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005076 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005077 parser.add_option(
5078 '--no-color',
5079 action='store_true',
5080 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005081 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005082 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005083 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005084
5085 author = RunGit(['config', 'user.email']).strip() or None
5086
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005087 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005088
5089 if args:
5090 if len(args) > 1:
5091 parser.error('Unknown args')
5092 base_branch = args[0]
5093 else:
5094 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005095 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005096
5097 change = cl.GetChange(base_branch, None)
5098 return owners_finder.OwnersFinder(
5099 [f.LocalPath() for f in
5100 cl.GetChange(base_branch, None).AffectedFiles()],
5101 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005102 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005103 disable_color=options.no_color).run()
5104
5105
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005106def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005107 """Generates a diff command."""
5108 # Generate diff for the current branch's changes.
5109 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5110 upstream_commit, '--' ]
5111
5112 if args:
5113 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005114 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005115 diff_cmd.append(arg)
5116 else:
5117 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005118
5119 return diff_cmd
5120
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005121def MatchingFileType(file_name, extensions):
5122 """Returns true if the file name ends with one of the given extensions."""
5123 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005124
enne@chromium.org555cfe42014-01-29 18:21:39 +00005125@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005126def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005127 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005128 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005129 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005130 parser.add_option('--full', action='store_true',
5131 help='Reformat the full content of all touched files')
5132 parser.add_option('--dry-run', action='store_true',
5133 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005134 parser.add_option('--python', action='store_true',
5135 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005136 parser.add_option('--diff', action='store_true',
5137 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005138 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005139
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005140 # git diff generates paths against the root of the repository. Change
5141 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005142 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005143 if rel_base_path:
5144 os.chdir(rel_base_path)
5145
digit@chromium.org29e47272013-05-17 17:01:46 +00005146 # Grab the merge-base commit, i.e. the upstream commit of the current
5147 # branch when it was created or the last time it was rebased. This is
5148 # to cover the case where the user may have called "git fetch origin",
5149 # moving the origin branch to a newer commit, but hasn't rebased yet.
5150 upstream_commit = None
5151 cl = Changelist()
5152 upstream_branch = cl.GetUpstreamBranch()
5153 if upstream_branch:
5154 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5155 upstream_commit = upstream_commit.strip()
5156
5157 if not upstream_commit:
5158 DieWithError('Could not find base commit for this branch. '
5159 'Are you in detached state?')
5160
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005161 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5162 diff_output = RunGit(changed_files_cmd)
5163 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005164 # Filter out files deleted by this CL
5165 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005166
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005167 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5168 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5169 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005170 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005171
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005172 top_dir = os.path.normpath(
5173 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5174
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005175 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5176 # formatted. This is used to block during the presubmit.
5177 return_value = 0
5178
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005179 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005180 # Locate the clang-format binary in the checkout
5181 try:
5182 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005183 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005184 DieWithError(e)
5185
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005186 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005187 cmd = [clang_format_tool]
5188 if not opts.dry_run and not opts.diff:
5189 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005190 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005191 if opts.diff:
5192 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005193 else:
5194 env = os.environ.copy()
5195 env['PATH'] = str(os.path.dirname(clang_format_tool))
5196 try:
5197 script = clang_format.FindClangFormatScriptInChromiumTree(
5198 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005199 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005200 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005201
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005202 cmd = [sys.executable, script, '-p0']
5203 if not opts.dry_run and not opts.diff:
5204 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005205
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005206 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5207 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005208
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005209 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5210 if opts.diff:
5211 sys.stdout.write(stdout)
5212 if opts.dry_run and len(stdout) > 0:
5213 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005214
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005215 # Similar code to above, but using yapf on .py files rather than clang-format
5216 # on C/C++ files
5217 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005218 yapf_tool = gclient_utils.FindExecutable('yapf')
5219 if yapf_tool is None:
5220 DieWithError('yapf not found in PATH')
5221
5222 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005223 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005224 cmd = [yapf_tool]
5225 if not opts.dry_run and not opts.diff:
5226 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005227 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005228 if opts.diff:
5229 sys.stdout.write(stdout)
5230 else:
5231 # TODO(sbc): yapf --lines mode still has some issues.
5232 # https://github.com/google/yapf/issues/154
5233 DieWithError('--python currently only works with --full')
5234
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005235 # Dart's formatter does not have the nice property of only operating on
5236 # modified chunks, so hard code full.
5237 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005238 try:
5239 command = [dart_format.FindDartFmtToolInChromiumTree()]
5240 if not opts.dry_run and not opts.diff:
5241 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005242 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005243
ppi@chromium.org6593d932016-03-03 15:41:15 +00005244 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005245 if opts.dry_run and stdout:
5246 return_value = 2
5247 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005248 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5249 'found in this checkout. Files in other languages are still '
5250 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005251
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005252 # Format GN build files. Always run on full build files for canonical form.
5253 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005254 cmd = ['gn', 'format' ]
5255 if opts.dry_run or opts.diff:
5256 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005257 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005258 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5259 shell=sys.platform == 'win32',
5260 cwd=top_dir)
5261 if opts.dry_run and gn_ret == 2:
5262 return_value = 2 # Not formatted.
5263 elif opts.diff and gn_ret == 2:
5264 # TODO this should compute and print the actual diff.
5265 print("This change has GN build file diff for " + gn_diff_file)
5266 elif gn_ret != 0:
5267 # For non-dry run cases (and non-2 return values for dry-run), a
5268 # nonzero error code indicates a failure, probably because the file
5269 # doesn't parse.
5270 DieWithError("gn format failed on " + gn_diff_file +
5271 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005272
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005273 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005274
5275
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005276@subcommand.usage('<codereview url or issue id>')
5277def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005278 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005279 _, args = parser.parse_args(args)
5280
5281 if len(args) != 1:
5282 parser.print_help()
5283 return 1
5284
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005285 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005286 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005287 parser.print_help()
5288 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005289 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005290
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005291 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005292 output = RunGit(['config', '--local', '--get-regexp',
5293 r'branch\..*\.%s' % issueprefix],
5294 error_ok=True)
5295 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005296 if issue == target_issue:
5297 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005298
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005299 branches = []
5300 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005301 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005302 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005303 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005304 return 1
5305 if len(branches) == 1:
5306 RunGit(['checkout', branches[0]])
5307 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005308 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005309 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005310 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005311 which = raw_input('Choose by index: ')
5312 try:
5313 RunGit(['checkout', branches[int(which)]])
5314 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005315 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005316 return 1
5317
5318 return 0
5319
5320
maruel@chromium.org29404b52014-09-08 22:58:00 +00005321def CMDlol(parser, args):
5322 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005323 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005324 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5325 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5326 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005327 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005328 return 0
5329
5330
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005331class OptionParser(optparse.OptionParser):
5332 """Creates the option parse and add --verbose support."""
5333 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005334 optparse.OptionParser.__init__(
5335 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005336 self.add_option(
5337 '-v', '--verbose', action='count', default=0,
5338 help='Use 2 times for more debugging info')
5339
5340 def parse_args(self, args=None, values=None):
5341 options, args = optparse.OptionParser.parse_args(self, args, values)
5342 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5343 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5344 return options, args
5345
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005347def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005348 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005349 print('\nYour python version %s is unsupported, please upgrade.\n' %
5350 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005351 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005352
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005353 # Reload settings.
5354 global settings
5355 settings = Settings()
5356
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005357 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005358 dispatcher = subcommand.CommandDispatcher(__name__)
5359 try:
5360 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005361 except auth.AuthenticationError as e:
5362 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005363 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005364 if e.code != 500:
5365 raise
5366 DieWithError(
5367 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5368 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005369 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005370
5371
5372if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005373 # These affect sys.stdout so do it outside of main() to simplify mocks in
5374 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005375 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005376 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005377 try:
5378 sys.exit(main(sys.argv[1:]))
5379 except KeyboardInterrupt:
5380 sys.stderr.write('interrupted\n')
5381 sys.exit(1)