blob: d8b90046c27dafb7ca4cf4e15d830c9754450290 [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
tandrii48df5812016-10-17 03:55:37 -07003067 if not keyvals.get('GERRIT_HOST', False):
3068 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003069 # Only server setting is required. Other settings can be absent.
3070 # In that case, we ignore errors raised during option deletion attempt.
3071 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003072 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003073 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3074 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003075 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003076 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003077 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3078 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003079 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003080 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003081 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003082 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003083 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3084 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003085
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003086 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003087 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003088
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003089 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003090 RunGit(['config', 'gerrit.squash-uploads',
3091 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003092
tandrii@chromium.org28253532016-04-14 13:46:56 +00003093 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003094 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003095 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3098 #should be of the form
3099 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3100 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3101 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3102 keyvals['ORIGIN_URL_CONFIG']])
3103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003104
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003105def urlretrieve(source, destination):
3106 """urllib is broken for SSL connections via a proxy therefore we
3107 can't use urllib.urlretrieve()."""
3108 with open(destination, 'w') as f:
3109 f.write(urllib2.urlopen(source).read())
3110
3111
ukai@chromium.org712d6102013-11-27 00:52:58 +00003112def hasSheBang(fname):
3113 """Checks fname is a #! script."""
3114 with open(fname) as f:
3115 return f.read(2).startswith('#!')
3116
3117
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003118# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3119def DownloadHooks(*args, **kwargs):
3120 pass
3121
3122
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003123def DownloadGerritHook(force):
3124 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003125
3126 Args:
3127 force: True to update hooks. False to install hooks if not present.
3128 """
3129 if not settings.GetIsGerrit():
3130 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003131 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003132 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3133 if not os.access(dst, os.X_OK):
3134 if os.path.exists(dst):
3135 if not force:
3136 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003137 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003138 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003139 if not hasSheBang(dst):
3140 DieWithError('Not a script: %s\n'
3141 'You need to download from\n%s\n'
3142 'into .git/hooks/commit-msg and '
3143 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003144 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3145 except Exception:
3146 if os.path.exists(dst):
3147 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003148 DieWithError('\nFailed to download hooks.\n'
3149 'You need to download from\n%s\n'
3150 'into .git/hooks/commit-msg and '
3151 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003152
3153
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003154
3155def GetRietveldCodereviewSettingsInteractively():
3156 """Prompt the user for settings."""
3157 server = settings.GetDefaultServerUrl(error_ok=True)
3158 prompt = 'Rietveld server (host[:port])'
3159 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3160 newserver = ask_for_data(prompt + ':')
3161 if not server and not newserver:
3162 newserver = DEFAULT_SERVER
3163 if newserver:
3164 newserver = gclient_utils.UpgradeToHttps(newserver)
3165 if newserver != server:
3166 RunGit(['config', 'rietveld.server', newserver])
3167
3168 def SetProperty(initial, caption, name, is_url):
3169 prompt = caption
3170 if initial:
3171 prompt += ' ("x" to clear) [%s]' % initial
3172 new_val = ask_for_data(prompt + ':')
3173 if new_val == 'x':
3174 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3175 elif new_val:
3176 if is_url:
3177 new_val = gclient_utils.UpgradeToHttps(new_val)
3178 if new_val != initial:
3179 RunGit(['config', 'rietveld.' + name, new_val])
3180
3181 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3182 SetProperty(settings.GetDefaultPrivateFlag(),
3183 'Private flag (rietveld only)', 'private', False)
3184 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3185 'tree-status-url', False)
3186 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3187 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3188 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3189 'run-post-upload-hook', False)
3190
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003191@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003192def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003193 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003194
tandrii5d0a0422016-09-14 06:24:35 -07003195 print('WARNING: git cl config works for Rietveld only')
3196 # TODO(tandrii): remove this once we switch to Gerrit.
3197 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003198 parser.add_option('--activate-update', action='store_true',
3199 help='activate auto-updating [rietveld] section in '
3200 '.git/config')
3201 parser.add_option('--deactivate-update', action='store_true',
3202 help='deactivate auto-updating [rietveld] section in '
3203 '.git/config')
3204 options, args = parser.parse_args(args)
3205
3206 if options.deactivate_update:
3207 RunGit(['config', 'rietveld.autoupdate', 'false'])
3208 return
3209
3210 if options.activate_update:
3211 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3212 return
3213
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003214 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003215 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003216 return 0
3217
3218 url = args[0]
3219 if not url.endswith('codereview.settings'):
3220 url = os.path.join(url, 'codereview.settings')
3221
3222 # Load code review settings and download hooks (if available).
3223 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3224 return 0
3225
3226
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003227def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003228 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003229 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3230 branch = ShortBranchName(branchref)
3231 _, args = parser.parse_args(args)
3232 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003233 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003234 return RunGit(['config', 'branch.%s.base-url' % branch],
3235 error_ok=False).strip()
3236 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003237 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003238 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3239 error_ok=False).strip()
3240
3241
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003242def color_for_status(status):
3243 """Maps a Changelist status to color, for CMDstatus and other tools."""
3244 return {
3245 'unsent': Fore.RED,
3246 'waiting': Fore.BLUE,
3247 'reply': Fore.YELLOW,
3248 'lgtm': Fore.GREEN,
3249 'commit': Fore.MAGENTA,
3250 'closed': Fore.CYAN,
3251 'error': Fore.WHITE,
3252 }.get(status, Fore.WHITE)
3253
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003254
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003255def get_cl_statuses(changes, fine_grained, max_processes=None):
3256 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003257
3258 If fine_grained is true, this will fetch CL statuses from the server.
3259 Otherwise, simply indicate if there's a matching url for the given branches.
3260
3261 If max_processes is specified, it is used as the maximum number of processes
3262 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3263 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003264
3265 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003266 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003267 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003268 upload.verbosity = 0
3269
3270 if fine_grained:
3271 # Process one branch synchronously to work through authentication, then
3272 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003273 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003274 def fetch(cl):
3275 try:
3276 return (cl, cl.GetStatus())
3277 except:
3278 # See http://crbug.com/629863.
3279 logging.exception('failed to fetch status for %s:', cl)
3280 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003281 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003282
tandriiea9514a2016-08-17 12:32:37 -07003283 changes_to_fetch = changes[1:]
3284 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003285 # Exit early if there was only one branch to fetch.
3286 return
3287
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003288 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003289 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003290 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003291 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003292
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003293 fetched_cls = set()
3294 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003295 while True:
3296 try:
3297 row = it.next(timeout=5)
3298 except multiprocessing.TimeoutError:
3299 break
3300
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003301 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003302 yield row
3303
3304 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003305 for cl in set(changes_to_fetch) - fetched_cls:
3306 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003307
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003308 else:
3309 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003310 for cl in changes:
3311 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003312
rmistry@google.com2dd99862015-06-22 12:22:18 +00003313
3314def upload_branch_deps(cl, args):
3315 """Uploads CLs of local branches that are dependents of the current branch.
3316
3317 If the local branch dependency tree looks like:
3318 test1 -> test2.1 -> test3.1
3319 -> test3.2
3320 -> test2.2 -> test3.3
3321
3322 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3323 run on the dependent branches in this order:
3324 test2.1, test3.1, test3.2, test2.2, test3.3
3325
3326 Note: This function does not rebase your local dependent branches. Use it when
3327 you make a change to the parent branch that will not conflict with its
3328 dependent branches, and you would like their dependencies updated in
3329 Rietveld.
3330 """
3331 if git_common.is_dirty_git_tree('upload-branch-deps'):
3332 return 1
3333
3334 root_branch = cl.GetBranch()
3335 if root_branch is None:
3336 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3337 'Get on a branch!')
3338 if not cl.GetIssue() or not cl.GetPatchset():
3339 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3340 'patchset dependencies without an uploaded CL.')
3341
3342 branches = RunGit(['for-each-ref',
3343 '--format=%(refname:short) %(upstream:short)',
3344 'refs/heads'])
3345 if not branches:
3346 print('No local branches found.')
3347 return 0
3348
3349 # Create a dictionary of all local branches to the branches that are dependent
3350 # on it.
3351 tracked_to_dependents = collections.defaultdict(list)
3352 for b in branches.splitlines():
3353 tokens = b.split()
3354 if len(tokens) == 2:
3355 branch_name, tracked = tokens
3356 tracked_to_dependents[tracked].append(branch_name)
3357
vapiera7fbd5a2016-06-16 09:17:49 -07003358 print()
3359 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003360 dependents = []
3361 def traverse_dependents_preorder(branch, padding=''):
3362 dependents_to_process = tracked_to_dependents.get(branch, [])
3363 padding += ' '
3364 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003365 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003366 dependents.append(dependent)
3367 traverse_dependents_preorder(dependent, padding)
3368 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003369 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003370
3371 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003372 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003373 return 0
3374
vapiera7fbd5a2016-06-16 09:17:49 -07003375 print('This command will checkout all dependent branches and run '
3376 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003377 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3378
andybons@chromium.org962f9462016-02-03 20:00:42 +00003379 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003380 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003381 args.extend(['-t', 'Updated patchset dependency'])
3382
rmistry@google.com2dd99862015-06-22 12:22:18 +00003383 # Record all dependents that failed to upload.
3384 failures = {}
3385 # Go through all dependents, checkout the branch and upload.
3386 try:
3387 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003388 print()
3389 print('--------------------------------------')
3390 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003391 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003392 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003393 try:
3394 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003395 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003396 failures[dependent_branch] = 1
3397 except: # pylint: disable=W0702
3398 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003399 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003400 finally:
3401 # Swap back to the original root branch.
3402 RunGit(['checkout', '-q', root_branch])
3403
vapiera7fbd5a2016-06-16 09:17:49 -07003404 print()
3405 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003406 for dependent_branch in dependents:
3407 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003408 print(' %s : %s' % (dependent_branch, upload_status))
3409 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003410
3411 return 0
3412
3413
kmarshall3bff56b2016-06-06 18:31:47 -07003414def CMDarchive(parser, args):
3415 """Archives and deletes branches associated with closed changelists."""
3416 parser.add_option(
3417 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003418 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003419 parser.add_option(
3420 '-f', '--force', action='store_true',
3421 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003422 parser.add_option(
3423 '-d', '--dry-run', action='store_true',
3424 help='Skip the branch tagging and removal steps.')
3425 parser.add_option(
3426 '-t', '--notags', action='store_true',
3427 help='Do not tag archived branches. '
3428 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003429
3430 auth.add_auth_options(parser)
3431 options, args = parser.parse_args(args)
3432 if args:
3433 parser.error('Unsupported args: %s' % ' '.join(args))
3434 auth_config = auth.extract_auth_config_from_options(options)
3435
3436 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3437 if not branches:
3438 return 0
3439
vapiera7fbd5a2016-06-16 09:17:49 -07003440 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003441 changes = [Changelist(branchref=b, auth_config=auth_config)
3442 for b in branches.splitlines()]
3443 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3444 statuses = get_cl_statuses(changes,
3445 fine_grained=True,
3446 max_processes=options.maxjobs)
3447 proposal = [(cl.GetBranch(),
3448 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3449 for cl, status in statuses
3450 if status == 'closed']
3451 proposal.sort()
3452
3453 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003454 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003455 return 0
3456
3457 current_branch = GetCurrentBranch()
3458
vapiera7fbd5a2016-06-16 09:17:49 -07003459 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003460 if options.notags:
3461 for next_item in proposal:
3462 print(' ' + next_item[0])
3463 else:
3464 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3465 for next_item in proposal:
3466 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003467
kmarshall9249e012016-08-23 12:02:16 -07003468 # Quit now on precondition failure or if instructed by the user, either
3469 # via an interactive prompt or by command line flags.
3470 if options.dry_run:
3471 print('\nNo changes were made (dry run).\n')
3472 return 0
3473 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003474 print('You are currently on a branch \'%s\' which is associated with a '
3475 'closed codereview issue, so archive cannot proceed. Please '
3476 'checkout another branch and run this command again.' %
3477 current_branch)
3478 return 1
kmarshall9249e012016-08-23 12:02:16 -07003479 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003480 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3481 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003482 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003483 return 1
3484
3485 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003486 if not options.notags:
3487 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003488 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003489
vapiera7fbd5a2016-06-16 09:17:49 -07003490 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003491
3492 return 0
3493
3494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003495def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003496 """Show status of changelists.
3497
3498 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003499 - Red not sent for review or broken
3500 - Blue waiting for review
3501 - Yellow waiting for you to reply to review
3502 - Green LGTM'ed
3503 - Magenta in the commit queue
3504 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003505
3506 Also see 'git cl comments'.
3507 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003509 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003510 parser.add_option('-f', '--fast', action='store_true',
3511 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003512 parser.add_option(
3513 '-j', '--maxjobs', action='store', type=int,
3514 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003515
3516 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003517 _add_codereview_issue_select_options(
3518 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003519 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003520 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003521 if args:
3522 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003523 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524
iannuccie53c9352016-08-17 14:40:40 -07003525 if options.issue is not None and not options.field:
3526 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003527
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003528 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003529 cl = Changelist(auth_config=auth_config, issue=options.issue,
3530 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003531 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533 elif options.field == 'id':
3534 issueid = cl.GetIssue()
3535 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003536 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 elif options.field == 'patch':
3538 patchset = cl.GetPatchset()
3539 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003541 elif options.field == 'status':
3542 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543 elif options.field == 'url':
3544 url = cl.GetIssueURL()
3545 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003546 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003547 return 0
3548
3549 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3550 if not branches:
3551 print('No local branch found.')
3552 return 0
3553
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003554 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003555 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003556 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003557 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003558 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003559 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003560 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003561
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003562 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003563 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3564 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3565 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003566 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003567 c, status = output.next()
3568 branch_statuses[c.GetBranch()] = status
3569 status = branch_statuses.pop(branch)
3570 url = cl.GetIssueURL()
3571 if url and (not status or status == 'error'):
3572 # The issue probably doesn't exist anymore.
3573 url += ' (broken)'
3574
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003575 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003576 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003577 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003578 color = ''
3579 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003580 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003582 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003583 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003584
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003585 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print()
3587 print('Current branch:',)
3588 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003589 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003591 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003592 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003593 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Issue description:')
3595 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003596 return 0
3597
3598
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003599def colorize_CMDstatus_doc():
3600 """To be called once in main() to add colors to git cl status help."""
3601 colors = [i for i in dir(Fore) if i[0].isupper()]
3602
3603 def colorize_line(line):
3604 for color in colors:
3605 if color in line.upper():
3606 # Extract whitespaces first and the leading '-'.
3607 indent = len(line) - len(line.lstrip(' ')) + 1
3608 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3609 return line
3610
3611 lines = CMDstatus.__doc__.splitlines()
3612 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3613
3614
phajdan.jre328cf92016-08-22 04:12:17 -07003615def write_json(path, contents):
3616 with open(path, 'w') as f:
3617 json.dump(contents, f)
3618
3619
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003620@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003621def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003622 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623
3624 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003625 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003626 parser.add_option('-r', '--reverse', action='store_true',
3627 help='Lookup the branch(es) for the specified issues. If '
3628 'no issues are specified, all branches with mapped '
3629 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003630 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003631 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003632 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003633 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634
dnj@chromium.org406c4402015-03-03 17:22:28 +00003635 if options.reverse:
3636 branches = RunGit(['for-each-ref', 'refs/heads',
3637 '--format=%(refname:short)']).splitlines()
3638
3639 # Reverse issue lookup.
3640 issue_branch_map = {}
3641 for branch in branches:
3642 cl = Changelist(branchref=branch)
3643 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3644 if not args:
3645 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003646 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003647 for issue in args:
3648 if not issue:
3649 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003650 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003651 print('Branch for issue number %s: %s' % (
3652 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003653 if options.json:
3654 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003655 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003656 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003657 if len(args) > 0:
3658 try:
3659 issue = int(args[0])
3660 except ValueError:
3661 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003662 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003663 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003665 if options.json:
3666 write_json(options.json, {
3667 'issue': cl.GetIssue(),
3668 'issue_url': cl.GetIssueURL(),
3669 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003670 return 0
3671
3672
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003673def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003674 """Shows or posts review comments for any changelist."""
3675 parser.add_option('-a', '--add-comment', dest='comment',
3676 help='comment to add to an issue')
3677 parser.add_option('-i', dest='issue',
3678 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003679 parser.add_option('-j', '--json-file',
3680 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003681 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003682 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003683 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003684
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003685 issue = None
3686 if options.issue:
3687 try:
3688 issue = int(options.issue)
3689 except ValueError:
3690 DieWithError('A review issue id is expected to be a number')
3691
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003692 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003693
3694 if options.comment:
3695 cl.AddComment(options.comment)
3696 return 0
3697
3698 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003699 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003700 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003701 summary.append({
3702 'date': message['date'],
3703 'lgtm': False,
3704 'message': message['text'],
3705 'not_lgtm': False,
3706 'sender': message['sender'],
3707 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003708 if message['disapproval']:
3709 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003710 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003711 elif message['approval']:
3712 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003713 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003714 elif message['sender'] == data['owner_email']:
3715 color = Fore.MAGENTA
3716 else:
3717 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003719 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003720 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003721 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003723 if options.json_file:
3724 with open(options.json_file, 'wb') as f:
3725 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003726 return 0
3727
3728
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003729@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003730def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003731 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003732 parser.add_option('-d', '--display', action='store_true',
3733 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003734 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003735 help='New description to set for this issue (- for stdin, '
3736 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003737 parser.add_option('-f', '--force', action='store_true',
3738 help='Delete any unpublished Gerrit edits for this issue '
3739 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003740
3741 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003742 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003743 options, args = parser.parse_args(args)
3744 _process_codereview_select_options(parser, options)
3745
3746 target_issue = None
3747 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003748 target_issue = ParseIssueNumberArgument(args[0])
3749 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003750 parser.print_help()
3751 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003752
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003753 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003754
martiniss6eda05f2016-06-30 10:18:35 -07003755 kwargs = {
3756 'auth_config': auth_config,
3757 'codereview': options.forced_codereview,
3758 }
3759 if target_issue:
3760 kwargs['issue'] = target_issue.issue
3761 if options.forced_codereview == 'rietveld':
3762 kwargs['rietveld_server'] = target_issue.hostname
3763
3764 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003765
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003766 if not cl.GetIssue():
3767 DieWithError('This branch has no associated changelist.')
3768 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003769
smut@google.com34fb6b12015-07-13 20:03:26 +00003770 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003772 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003773
3774 if options.new_description:
3775 text = options.new_description
3776 if text == '-':
3777 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003778 elif text == '+':
3779 base_branch = cl.GetCommonAncestorWithUpstream()
3780 change = cl.GetChange(base_branch, None, local_description=True)
3781 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003782
3783 description.set_description(text)
3784 else:
3785 description.prompt()
3786
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003787 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003788 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003789 return 0
3790
3791
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003792def CreateDescriptionFromLog(args):
3793 """Pulls out the commit log to use as a base for the CL description."""
3794 log_args = []
3795 if len(args) == 1 and not args[0].endswith('.'):
3796 log_args = [args[0] + '..']
3797 elif len(args) == 1 and args[0].endswith('...'):
3798 log_args = [args[0][:-1]]
3799 elif len(args) == 2:
3800 log_args = [args[0] + '..' + args[1]]
3801 else:
3802 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003803 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003804
3805
thestig@chromium.org44202a22014-03-11 19:22:18 +00003806def CMDlint(parser, args):
3807 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003808 parser.add_option('--filter', action='append', metavar='-x,+y',
3809 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003810 auth.add_auth_options(parser)
3811 options, args = parser.parse_args(args)
3812 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003813
3814 # Access to a protected member _XX of a client class
3815 # pylint: disable=W0212
3816 try:
3817 import cpplint
3818 import cpplint_chromium
3819 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003820 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003821 return 1
3822
3823 # Change the current working directory before calling lint so that it
3824 # shows the correct base.
3825 previous_cwd = os.getcwd()
3826 os.chdir(settings.GetRoot())
3827 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003828 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003829 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3830 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003831 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003832 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003833 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003834
3835 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003836 command = args + files
3837 if options.filter:
3838 command = ['--filter=' + ','.join(options.filter)] + command
3839 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003840
3841 white_regex = re.compile(settings.GetLintRegex())
3842 black_regex = re.compile(settings.GetLintIgnoreRegex())
3843 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3844 for filename in filenames:
3845 if white_regex.match(filename):
3846 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003847 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003848 else:
3849 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3850 extra_check_functions)
3851 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003852 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003853 finally:
3854 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003855 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003856 if cpplint._cpplint_state.error_count != 0:
3857 return 1
3858 return 0
3859
3860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003861def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003862 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003863 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003865 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003866 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003867 auth.add_auth_options(parser)
3868 options, args = parser.parse_args(args)
3869 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870
sbc@chromium.org71437c02015-04-09 19:29:40 +00003871 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003872 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003873 return 1
3874
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003875 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 if args:
3877 base_branch = args[0]
3878 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003879 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003880 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003882 cl.RunHook(
3883 committing=not options.upload,
3884 may_prompt=False,
3885 verbose=options.verbose,
3886 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003887 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003888
3889
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003890def GenerateGerritChangeId(message):
3891 """Returns Ixxxxxx...xxx change id.
3892
3893 Works the same way as
3894 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3895 but can be called on demand on all platforms.
3896
3897 The basic idea is to generate git hash of a state of the tree, original commit
3898 message, author/committer info and timestamps.
3899 """
3900 lines = []
3901 tree_hash = RunGitSilent(['write-tree'])
3902 lines.append('tree %s' % tree_hash.strip())
3903 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3904 if code == 0:
3905 lines.append('parent %s' % parent.strip())
3906 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3907 lines.append('author %s' % author.strip())
3908 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3909 lines.append('committer %s' % committer.strip())
3910 lines.append('')
3911 # Note: Gerrit's commit-hook actually cleans message of some lines and
3912 # whitespace. This code is not doing this, but it clearly won't decrease
3913 # entropy.
3914 lines.append(message)
3915 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3916 stdin='\n'.join(lines))
3917 return 'I%s' % change_hash.strip()
3918
3919
wittman@chromium.org455dc922015-01-26 20:15:50 +00003920def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3921 """Computes the remote branch ref to use for the CL.
3922
3923 Args:
3924 remote (str): The git remote for the CL.
3925 remote_branch (str): The git remote branch for the CL.
3926 target_branch (str): The target branch specified by the user.
3927 pending_prefix (str): The pending prefix from the settings.
3928 """
3929 if not (remote and remote_branch):
3930 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003931
wittman@chromium.org455dc922015-01-26 20:15:50 +00003932 if target_branch:
3933 # Cannonicalize branch references to the equivalent local full symbolic
3934 # refs, which are then translated into the remote full symbolic refs
3935 # below.
3936 if '/' not in target_branch:
3937 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3938 else:
3939 prefix_replacements = (
3940 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3941 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3942 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3943 )
3944 match = None
3945 for regex, replacement in prefix_replacements:
3946 match = re.search(regex, target_branch)
3947 if match:
3948 remote_branch = target_branch.replace(match.group(0), replacement)
3949 break
3950 if not match:
3951 # This is a branch path but not one we recognize; use as-is.
3952 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003953 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3954 # Handle the refs that need to land in different refs.
3955 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003956
wittman@chromium.org455dc922015-01-26 20:15:50 +00003957 # Create the true path to the remote branch.
3958 # Does the following translation:
3959 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3960 # * refs/remotes/origin/master -> refs/heads/master
3961 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3962 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3963 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3964 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3965 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3966 'refs/heads/')
3967 elif remote_branch.startswith('refs/remotes/branch-heads'):
3968 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3969 # If a pending prefix exists then replace refs/ with it.
3970 if pending_prefix:
3971 remote_branch = remote_branch.replace('refs/', pending_prefix)
3972 return remote_branch
3973
3974
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003975def cleanup_list(l):
3976 """Fixes a list so that comma separated items are put as individual items.
3977
3978 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3979 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3980 """
3981 items = sum((i.split(',') for i in l), [])
3982 stripped_items = (i.strip() for i in items)
3983 return sorted(filter(None, stripped_items))
3984
3985
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003986@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003987def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003988 """Uploads the current changelist to codereview.
3989
3990 Can skip dependency patchset uploads for a branch by running:
3991 git config branch.branch_name.skip-deps-uploads True
3992 To unset run:
3993 git config --unset branch.branch_name.skip-deps-uploads
3994 Can also set the above globally by using the --global flag.
3995 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003996 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3997 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003998 parser.add_option('--bypass-watchlists', action='store_true',
3999 dest='bypass_watchlists',
4000 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004001 parser.add_option('-f', action='store_true', dest='force',
4002 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004003 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004004 parser.add_option('-b', '--bug',
4005 help='pre-populate the bug number(s) for this issue. '
4006 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004007 parser.add_option('--message-file', dest='message_file',
4008 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004009 parser.add_option('-t', dest='title',
4010 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004011 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004012 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004013 help='reviewer email addresses')
4014 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004015 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004016 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004017 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004018 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004019 parser.add_option('--emulate_svn_auto_props',
4020 '--emulate-svn-auto-props',
4021 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004022 dest="emulate_svn_auto_props",
4023 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004024 parser.add_option('-c', '--use-commit-queue', action='store_true',
4025 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004026 parser.add_option('--private', action='store_true',
4027 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004028 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004029 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004030 metavar='TARGET',
4031 help='Apply CL to remote ref TARGET. ' +
4032 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004033 parser.add_option('--squash', action='store_true',
4034 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004035 parser.add_option('--no-squash', action='store_true',
4036 help='Don\'t squash multiple commits into one ' +
4037 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004038 parser.add_option('--topic', default=None,
4039 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004040 parser.add_option('--email', default=None,
4041 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004042 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4043 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004044 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4045 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004046 help='Send the patchset to do a CQ dry run right after '
4047 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004048 parser.add_option('--dependencies', action='store_true',
4049 help='Uploads CLs of all the local branches that depend on '
4050 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004051
rmistry@google.com2dd99862015-06-22 12:22:18 +00004052 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004053 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004054 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004055 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004056 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004057 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004058 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004059
sbc@chromium.org71437c02015-04-09 19:29:40 +00004060 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004061 return 1
4062
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004063 options.reviewers = cleanup_list(options.reviewers)
4064 options.cc = cleanup_list(options.cc)
4065
tandriib80458a2016-06-23 12:20:07 -07004066 if options.message_file:
4067 if options.message:
4068 parser.error('only one of --message and --message-file allowed.')
4069 options.message = gclient_utils.FileRead(options.message_file)
4070 options.message_file = None
4071
tandrii4d0545a2016-07-06 03:56:49 -07004072 if options.cq_dry_run and options.use_commit_queue:
4073 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4074
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004075 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4076 settings.GetIsGerrit()
4077
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004078 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004079 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004080
4081
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004082def IsSubmoduleMergeCommit(ref):
4083 # When submodules are added to the repo, we expect there to be a single
4084 # non-git-svn merge commit at remote HEAD with a signature comment.
4085 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004086 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004087 return RunGit(cmd) != ''
4088
4089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004090def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004091 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004093 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4094 upstream and closes the issue automatically and atomically.
4095
4096 Otherwise (in case of Rietveld):
4097 Squashes branch into a single commit.
4098 Updates changelog with metadata (e.g. pointer to review).
4099 Pushes/dcommits the code upstream.
4100 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004101 """
4102 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4103 help='bypass upload presubmit hook')
4104 parser.add_option('-m', dest='message',
4105 help="override review description")
4106 parser.add_option('-f', action='store_true', dest='force',
4107 help="force yes to questions (don't prompt)")
4108 parser.add_option('-c', dest='contributor',
4109 help="external contributor for patch (appended to " +
4110 "description and used as author for git). Should be " +
4111 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004112 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004113 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004115 auth_config = auth.extract_auth_config_from_options(options)
4116
4117 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004119 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4120 if cl.IsGerrit():
4121 if options.message:
4122 # This could be implemented, but it requires sending a new patch to
4123 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4124 # Besides, Gerrit has the ability to change the commit message on submit
4125 # automatically, thus there is no need to support this option (so far?).
4126 parser.error('-m MESSAGE option is not supported for Gerrit.')
4127 if options.contributor:
4128 parser.error(
4129 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4130 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4131 'the contributor\'s "name <email>". If you can\'t upload such a '
4132 'commit for review, contact your repository admin and request'
4133 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004134 if not cl.GetIssue():
4135 DieWithError('You must upload the issue first to Gerrit.\n'
4136 ' If you would rather have `git cl land` upload '
4137 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004138 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4139 options.verbose)
4140
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004141 current = cl.GetBranch()
4142 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4143 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004144 print()
4145 print('Attempting to push branch %r into another local branch!' % current)
4146 print()
4147 print('Either reparent this branch on top of origin/master:')
4148 print(' git reparent-branch --root')
4149 print()
4150 print('OR run `git rebase-update` if you think the parent branch is ')
4151 print('already committed.')
4152 print()
4153 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004154 return 1
4155
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004156 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004157 # Default to merging against our best guess of the upstream branch.
4158 args = [cl.GetUpstreamBranch()]
4159
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004160 if options.contributor:
4161 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004162 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004163 return 1
4164
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004165 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004166 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004167
sbc@chromium.org71437c02015-04-09 19:29:40 +00004168 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004169 return 1
4170
4171 # This rev-list syntax means "show all commits not in my branch that
4172 # are in base_branch".
4173 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4174 base_branch]).splitlines()
4175 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004176 print('Base branch "%s" has %d commits '
4177 'not in this branch.' % (base_branch, len(upstream_commits)))
4178 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004179 return 1
4180
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004181 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004182 svn_head = None
4183 if cmd == 'dcommit' or base_has_submodules:
4184 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4185 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004187 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004188 # If the base_head is a submodule merge commit, the first parent of the
4189 # base_head should be a git-svn commit, which is what we're interested in.
4190 base_svn_head = base_branch
4191 if base_has_submodules:
4192 base_svn_head += '^1'
4193
4194 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print('This branch has %d additional commits not upstreamed yet.'
4197 % len(extra_commits.splitlines()))
4198 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4199 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200 return 1
4201
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004202 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004203 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004204 author = None
4205 if options.contributor:
4206 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004207 hook_results = cl.RunHook(
4208 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004209 may_prompt=not options.force,
4210 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004211 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004212 if not hook_results.should_continue():
4213 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004214
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004215 # Check the tree status if the tree status URL is set.
4216 status = GetTreeStatus()
4217 if 'closed' == status:
4218 print('The tree is closed. Please wait for it to reopen. Use '
4219 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4220 return 1
4221 elif 'unknown' == status:
4222 print('Unable to determine tree status. Please verify manually and '
4223 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4224 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004225
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004226 change_desc = ChangeDescription(options.message)
4227 if not change_desc.description and cl.GetIssue():
4228 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004230 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004231 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004232 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004233 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004234 print('No description set.')
4235 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004236 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004238 # Keep a separate copy for the commit message, because the commit message
4239 # contains the link to the Rietveld issue, while the Rietveld message contains
4240 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004241 # Keep a separate copy for the commit message.
4242 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004243 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004244
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004245 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004246 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004247 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004248 # after it. Add a period on a new line to circumvent this. Also add a space
4249 # before the period to make sure that Gitiles continues to correctly resolve
4250 # the URL.
4251 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004252 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004253 commit_desc.append_footer('Patch from %s.' % options.contributor)
4254
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004255 print('Description:')
4256 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004258 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004259 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004260 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004261
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004262 # We want to squash all this branch's commits into one commit with the proper
4263 # description. We do this by doing a "reset --soft" to the base branch (which
4264 # keeps the working copy the same), then dcommitting that. If origin/master
4265 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4266 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004268 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4269 # Delete the branches if they exist.
4270 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4271 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4272 result = RunGitWithCode(showref_cmd)
4273 if result[0] == 0:
4274 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275
4276 # We might be in a directory that's present in this branch but not in the
4277 # trunk. Move up to the top of the tree so that git commands that expect a
4278 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004279 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004280 if rel_base_path:
4281 os.chdir(rel_base_path)
4282
4283 # Stuff our change into the merge branch.
4284 # We wrap in a try...finally block so if anything goes wrong,
4285 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004286 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004287 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004288 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004289 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004290 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004291 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004292 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004294 RunGit(
4295 [
4296 'commit', '--author', options.contributor,
4297 '-m', commit_desc.description,
4298 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004300 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004301 if base_has_submodules:
4302 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4303 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4304 RunGit(['checkout', CHERRY_PICK_BRANCH])
4305 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004306 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004307 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004308 mirror = settings.GetGitMirror(remote)
4309 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004310 pending_prefix = settings.GetPendingRefPrefix()
4311 if not pending_prefix or branch.startswith(pending_prefix):
4312 # If not using refs/pending/heads/* at all, or target ref is already set
4313 # to pending, then push to the target ref directly.
4314 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004315 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004316 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004317 else:
4318 # Cherry-pick the change on top of pending ref and then push it.
4319 assert branch.startswith('refs/'), branch
4320 assert pending_prefix[-1] == '/', pending_prefix
4321 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004322 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004323 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004324 if retcode == 0:
4325 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004326 else:
4327 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004328 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004329 'svn', 'dcommit',
4330 '-C%s' % options.similarity,
4331 '--no-rebase', '--rmdir',
4332 ]
4333 if settings.GetForceHttpsCommitUrl():
4334 # Allow forcing https commit URLs for some projects that don't allow
4335 # committing to http URLs (like Google Code).
4336 remote_url = cl.GetGitSvnRemoteUrl()
4337 if urlparse.urlparse(remote_url).scheme == 'http':
4338 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004339 cmd_args.append('--commit-url=%s' % remote_url)
4340 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004341 if 'Committed r' in output:
4342 revision = re.match(
4343 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4344 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345 finally:
4346 # And then swap back to the original branch and clean up.
4347 RunGit(['checkout', '-q', cl.GetBranch()])
4348 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004349 if base_has_submodules:
4350 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004352 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004353 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004354 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004355
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004356 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004357 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004358 try:
4359 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4360 # We set pushed_to_pending to False, since it made it all the way to the
4361 # real ref.
4362 pushed_to_pending = False
4363 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004364 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004367 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004368 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004369 if not to_pending:
4370 if viewvc_url and revision:
4371 change_desc.append_footer(
4372 'Committed: %s%s' % (viewvc_url, revision))
4373 elif revision:
4374 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004375 print('Closing issue '
4376 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004377 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004379 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004380 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004381 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004382 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004383 if options.bypass_hooks:
4384 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4385 else:
4386 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004387 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004388
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004389 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004390 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('The commit is in the pending queue (%s).' % pending_ref)
4392 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4393 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004394
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004395 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4396 if os.path.isfile(hook):
4397 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004398
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004399 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400
4401
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004402def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004403 print()
4404 print('Waiting for commit to be landed on %s...' % real_ref)
4405 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004406 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4407 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004408 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004409
4410 loop = 0
4411 while True:
4412 sys.stdout.write('fetching (%d)... \r' % loop)
4413 sys.stdout.flush()
4414 loop += 1
4415
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004416 if mirror:
4417 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004418 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4419 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4420 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4421 for commit in commits.splitlines():
4422 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004423 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004424 return commit
4425
4426 current_rev = to_rev
4427
4428
tandriibf429402016-09-14 07:09:12 -07004429def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004430 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4431
4432 Returns:
4433 (retcode of last operation, output log of last operation).
4434 """
4435 assert pending_ref.startswith('refs/'), pending_ref
4436 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4437 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4438 code = 0
4439 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004440 max_attempts = 3
4441 attempts_left = max_attempts
4442 while attempts_left:
4443 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004444 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004445 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004446
4447 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004449 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004450 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004451 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004452 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004453 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004454 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004455 continue
4456
4457 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004458 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004459 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004460 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004461 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004462 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4463 'the following files have merge conflicts:' % pending_ref)
4464 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4465 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004466 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004467 return code, out
4468
4469 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004471 code, out = RunGitWithCode(
4472 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4473 if code == 0:
4474 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004475 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004476 return code, out
4477
vapiera7fbd5a2016-06-16 09:17:49 -07004478 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004479 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004480 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004481 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004482 print('Fatal push error. Make sure your .netrc credentials and git '
4483 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004484 return code, out
4485
vapiera7fbd5a2016-06-16 09:17:49 -07004486 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004487 return code, out
4488
4489
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004490def IsFatalPushFailure(push_stdout):
4491 """True if retrying push won't help."""
4492 return '(prohibited by Gerrit)' in push_stdout
4493
4494
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004495@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004497 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004498 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004499 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004500 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004501 message = """This repository appears to be a git-svn mirror, but we
4502don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004503 else:
4504 message = """This doesn't appear to be an SVN repository.
4505If your project has a true, writeable git repository, you probably want to run
4506'git cl land' instead.
4507If your project has a git mirror of an upstream SVN master, you probably need
4508to run 'git svn init'.
4509
4510Using the wrong command might cause your commit to appear to succeed, and the
4511review to be closed, without actually landing upstream. If you choose to
4512proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004513 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004514 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004515 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4516 'Please let us know of this project you are committing to:'
4517 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518 return SendUpstream(parser, args, 'dcommit')
4519
4520
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004521@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004522def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004523 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004524 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004525 print('This appears to be an SVN repository.')
4526 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004527 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004528 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004529 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530
4531
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004532@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004534 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535 parser.add_option('-b', dest='newbranch',
4536 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004537 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004538 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004539 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4540 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004541 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004542 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004543 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004544 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004546 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004547
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004548
4549 group = optparse.OptionGroup(
4550 parser,
4551 'Options for continuing work on the current issue uploaded from a '
4552 'different clone (e.g. different machine). Must be used independently '
4553 'from the other options. No issue number should be specified, and the '
4554 'branch must have an issue number associated with it')
4555 group.add_option('--reapply', action='store_true', dest='reapply',
4556 help='Reset the branch and reapply the issue.\n'
4557 'CAUTION: This will undo any local changes in this '
4558 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004559
4560 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004561 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004562 parser.add_option_group(group)
4563
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004564 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004565 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004566 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004567 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004568 auth_config = auth.extract_auth_config_from_options(options)
4569
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004570
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004571 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004572 if options.newbranch:
4573 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004574 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004575 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004576
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004577 cl = Changelist(auth_config=auth_config,
4578 codereview=options.forced_codereview)
4579 if not cl.GetIssue():
4580 parser.error('current branch must have an associated issue')
4581
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004582 upstream = cl.GetUpstreamBranch()
4583 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004584 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004585
4586 RunGit(['reset', '--hard', upstream])
4587 if options.pull:
4588 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004589
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004590 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4591 options.directory)
4592
4593 if len(args) != 1 or not args[0]:
4594 parser.error('Must specify issue number or url')
4595
4596 # We don't want uncommitted changes mixed up with the patch.
4597 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004598 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004599
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004600 if options.newbranch:
4601 if options.force:
4602 RunGit(['branch', '-D', options.newbranch],
4603 stderr=subprocess2.PIPE, error_ok=True)
4604 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004605 elif not GetCurrentBranch():
4606 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004607
4608 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4609
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004610 if cl.IsGerrit():
4611 if options.reject:
4612 parser.error('--reject is not supported with Gerrit codereview.')
4613 if options.nocommit:
4614 parser.error('--nocommit is not supported with Gerrit codereview.')
4615 if options.directory:
4616 parser.error('--directory is not supported with Gerrit codereview.')
4617
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004618 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004619 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004620
4621
4622def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004623 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624 # Provide a wrapper for git svn rebase to help avoid accidental
4625 # git svn dcommit.
4626 # It's the only command that doesn't use parser at all since we just defer
4627 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004628
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004629 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004630
4631
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004632def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633 """Fetches the tree status and returns either 'open', 'closed',
4634 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004635 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004636 if url:
4637 status = urllib2.urlopen(url).read().lower()
4638 if status.find('closed') != -1 or status == '0':
4639 return 'closed'
4640 elif status.find('open') != -1 or status == '1':
4641 return 'open'
4642 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 return 'unset'
4644
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646def GetTreeStatusReason():
4647 """Fetches the tree status from a json url and returns the message
4648 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004649 url = settings.GetTreeStatusUrl()
4650 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004651 connection = urllib2.urlopen(json_url)
4652 status = json.loads(connection.read())
4653 connection.close()
4654 return status['message']
4655
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004656
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004657def GetBuilderMaster(bot_list):
4658 """For a given builder, fetch the master from AE if available."""
4659 map_url = 'https://builders-map.appspot.com/'
4660 try:
4661 master_map = json.load(urllib2.urlopen(map_url))
4662 except urllib2.URLError as e:
4663 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4664 (map_url, e))
4665 except ValueError as e:
4666 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4667 if not master_map:
4668 return None, 'Failed to build master map.'
4669
4670 result_master = ''
4671 for bot in bot_list:
4672 builder = bot.split(':', 1)[0]
4673 master_list = master_map.get(builder, [])
4674 if not master_list:
4675 return None, ('No matching master for builder %s.' % builder)
4676 elif len(master_list) > 1:
4677 return None, ('The builder name %s exists in multiple masters %s.' %
4678 (builder, master_list))
4679 else:
4680 cur_master = master_list[0]
4681 if not result_master:
4682 result_master = cur_master
4683 elif result_master != cur_master:
4684 return None, 'The builders do not belong to the same master.'
4685 return result_master, None
4686
4687
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004688def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004689 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004690 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691 status = GetTreeStatus()
4692 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004693 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694 return 2
4695
vapiera7fbd5a2016-06-16 09:17:49 -07004696 print('The tree is %s' % status)
4697 print()
4698 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004699 if status != 'open':
4700 return 1
4701 return 0
4702
4703
maruel@chromium.org15192402012-09-06 12:38:29 +00004704def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004705 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4706 """
tandrii1838bad2016-10-06 00:10:52 -07004707 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004708 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004709 '-b', '--bot', action='append',
4710 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4711 'times to specify multiple builders. ex: '
4712 '"-b win_rel -b win_layout". See '
4713 'the try server waterfall for the builders name and the tests '
4714 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004715 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004716 '-m', '--master', default='',
4717 help=('Specify a try master where to run the tries.'))
tandriif7b29d42016-10-07 08:45:41 -07004718 # TODO(tandrii,nodir): add -B --bucket flag.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004719 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004720 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004721 help='Revision to use for the try job; default: the revision will '
4722 'be determined by the try recipe that builder runs, which usually '
4723 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004724 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004725 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004726 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004727 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004728 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004729 '--project',
4730 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004731 'in recipe to determine to which repository or directory to '
4732 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004733 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004734 '-p', '--property', dest='properties', action='append', default=[],
4735 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004736 'key2=value2 etc. The value will be treated as '
4737 'json if decodable, or as string otherwise. '
4738 'NOTE: using this may make your try job not usable for CQ, '
4739 'which will then schedule another try job with default properties')
4740 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004741 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004742 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004743 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004744 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4745 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004746 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004747 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004748 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004749 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004750
machenbach@chromium.org45453142015-09-15 08:45:22 +00004751 # Make sure that all properties are prop=value pairs.
4752 bad_params = [x for x in options.properties if '=' not in x]
4753 if bad_params:
4754 parser.error('Got properties with missing "=": %s' % bad_params)
4755
maruel@chromium.org15192402012-09-06 12:38:29 +00004756 if args:
4757 parser.error('Unknown arguments: %s' % args)
4758
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004759 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004760 if not cl.GetIssue():
4761 parser.error('Need to upload first')
4762
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004763 if cl.IsGerrit():
4764 parser.error(
4765 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4766 'If your project has Commit Queue, dry run is a workaround:\n'
4767 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004768
tandriie113dfd2016-10-11 10:20:12 -07004769 error_message = cl.CannotTriggerTryJobReason()
4770 if error_message:
4771 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004772
maruel@chromium.org15192402012-09-06 12:38:29 +00004773 if not options.name:
4774 options.name = cl.GetBranch()
4775
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004776 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004777 options.master, err_msg = GetBuilderMaster(options.bot)
4778 if err_msg:
4779 parser.error('Tryserver master cannot be found because: %s\n'
4780 'Please manually specify the tryserver master'
4781 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004782
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004783 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004784 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004785 if not options.bot:
4786 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004787
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004788 # Get try masters from PRESUBMIT.py files.
4789 masters = presubmit_support.DoGetTryMasters(
4790 change,
4791 change.LocalPaths(),
4792 settings.GetRoot(),
4793 None,
4794 None,
4795 options.verbose,
4796 sys.stdout)
4797 if masters:
4798 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004799
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004800 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4801 options.bot = presubmit_support.DoGetTrySlaves(
4802 change,
4803 change.LocalPaths(),
4804 settings.GetRoot(),
4805 None,
4806 None,
4807 options.verbose,
4808 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004809
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004810 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004811 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004812
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004813 builders_and_tests = {}
4814 # TODO(machenbach): The old style command-line options don't support
4815 # multiple try masters yet.
4816 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4817 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4818
4819 for bot in old_style:
4820 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004821 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004822 elif ',' in bot:
4823 parser.error('Specify one bot per --bot flag')
4824 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004825 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004826
4827 for bot, tests in new_style:
4828 builders_and_tests.setdefault(bot, []).extend(tests)
4829
4830 # Return a master map with one master to be backwards compatible. The
4831 # master name defaults to an empty string, which will cause the master
4832 # not to be set on rietveld (deprecated).
4833 return {options.master: builders_and_tests}
4834
4835 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004836 if not masters:
4837 # Default to triggering Dry Run (see http://crbug.com/625697).
4838 if options.verbose:
4839 print('git cl try with no bots now defaults to CQ Dry Run.')
4840 try:
4841 cl.SetCQState(_CQState.DRY_RUN)
4842 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4843 return 0
4844 except KeyboardInterrupt:
4845 raise
4846 except:
4847 print('WARNING: failed to trigger CQ Dry Run.\n'
4848 'Either:\n'
4849 ' * your project has no CQ\n'
4850 ' * you don\'t have permission to trigger Dry Run\n'
4851 ' * bug in this code (see stack trace below).\n'
4852 'Consider specifying which bots to trigger manually '
4853 'or asking your project owners for permissions '
4854 'or contacting Chrome Infrastructure team at '
4855 'https://www.chromium.org/infra\n\n')
4856 # Still raise exception so that stack trace is printed.
4857 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004858
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004859 for builders in masters.itervalues():
4860 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004861 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004862 'of bot requires an initial job from a parent (usually a builder). '
4863 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004864 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004865 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004866
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004867 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004868 if patchset != cl.GetPatchset():
4869 print('Warning: Codereview server has newer patchsets (%s) than most '
4870 'recent upload from local checkout (%s). Did a previous upload '
4871 'fail?\n'
4872 'By default, git cl try uses the latest patchset from '
4873 'codereview, continuing to use patchset %s.\n' %
4874 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004875 try:
tandriide281ae2016-10-12 06:02:30 -07004876 _trigger_try_jobs(auth_config, cl, masters, options, 'git_cl_try',
4877 patchset)
tandrii568043b2016-10-11 07:49:18 -07004878 except BuildbucketResponseException as ex:
4879 print('ERROR: %s' % ex)
4880 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004881 return 0
4882
4883
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004884def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004885 """Prints info about try jobs associated with current CL."""
4886 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004887 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004888 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004889 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004890 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004891 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004892 '--color', action='store_true', default=setup_color.IS_TTY,
4893 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004894 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004895 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4896 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004897 group.add_option(
4898 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004899 parser.add_option_group(group)
4900 auth.add_auth_options(parser)
4901 options, args = parser.parse_args(args)
4902 if args:
4903 parser.error('Unrecognized args: %s' % ' '.join(args))
4904
4905 auth_config = auth.extract_auth_config_from_options(options)
4906 cl = Changelist(auth_config=auth_config)
4907 if not cl.GetIssue():
4908 parser.error('Need to upload first')
4909
tandrii221ab252016-10-06 08:12:04 -07004910 patchset = options.patchset
4911 if not patchset:
4912 patchset = cl.GetMostRecentPatchset()
4913 if not patchset:
4914 parser.error('Codereview doesn\'t know about issue %s. '
4915 'No access to issue or wrong issue number?\n'
4916 'Either upload first, or pass --patchset explicitely' %
4917 cl.GetIssue())
4918
4919 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004920 print('Warning: Codereview server has newer patchsets (%s) than most '
4921 'recent upload from local checkout (%s). Did a previous upload '
4922 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004923 'By default, git cl try-results uses the latest patchset from '
4924 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004925 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004926 try:
tandrii221ab252016-10-06 08:12:04 -07004927 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004928 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004929 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004931 if options.json:
4932 write_try_results_json(options.json, jobs)
4933 else:
4934 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004935 return 0
4936
4937
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004938@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004939def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004940 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004941 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004942 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004943 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004945 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004946 if args:
4947 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004948 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004949 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004950 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004951 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004952
4953 # Clear configured merge-base, if there is one.
4954 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004955 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004956 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004957 return 0
4958
4959
thestig@chromium.org00858c82013-12-02 23:08:03 +00004960def CMDweb(parser, args):
4961 """Opens the current CL in the web browser."""
4962 _, args = parser.parse_args(args)
4963 if args:
4964 parser.error('Unrecognized args: %s' % ' '.join(args))
4965
4966 issue_url = Changelist().GetIssueURL()
4967 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004968 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004969 return 1
4970
4971 webbrowser.open(issue_url)
4972 return 0
4973
4974
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004975def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004976 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004977 parser.add_option('-d', '--dry-run', action='store_true',
4978 help='trigger in dry run mode')
4979 parser.add_option('-c', '--clear', action='store_true',
4980 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004981 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004982 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004983 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004984 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004985 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004986 if args:
4987 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004988 if options.dry_run and options.clear:
4989 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4990
iannuccie53c9352016-08-17 14:40:40 -07004991 cl = Changelist(auth_config=auth_config, issue=options.issue,
4992 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004993 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004994 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004995 elif options.dry_run:
4996 state = _CQState.DRY_RUN
4997 else:
4998 state = _CQState.COMMIT
4999 if not cl.GetIssue():
5000 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005001 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005002 return 0
5003
5004
groby@chromium.org411034a2013-02-26 15:12:01 +00005005def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005006 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005007 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005008 auth.add_auth_options(parser)
5009 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005010 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005011 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005012 if args:
5013 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005014 cl = Changelist(auth_config=auth_config, issue=options.issue,
5015 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005016 # Ensure there actually is an issue to close.
5017 cl.GetDescription()
5018 cl.CloseIssue()
5019 return 0
5020
5021
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005022def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005023 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005024 parser.add_option(
5025 '--stat',
5026 action='store_true',
5027 dest='stat',
5028 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005029 auth.add_auth_options(parser)
5030 options, args = parser.parse_args(args)
5031 auth_config = auth.extract_auth_config_from_options(options)
5032 if args:
5033 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005034
5035 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005036 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005037 # Staged changes would be committed along with the patch from last
5038 # upload, hence counted toward the "last upload" side in the final
5039 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005040 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005041 return 1
5042
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005043 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005044 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005045 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005046 if not issue:
5047 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005048 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005049 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005050
5051 # Create a new branch based on the merge-base
5052 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005053 # Clear cached branch in cl object, to avoid overwriting original CL branch
5054 # properties.
5055 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005056 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005057 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005058 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005059 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005060 return rtn
5061
wychen@chromium.org06928532015-02-03 02:11:29 +00005062 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005063 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005064 cmd = ['git', 'diff']
5065 if options.stat:
5066 cmd.append('--stat')
5067 cmd.extend([TMP_BRANCH, branch, '--'])
5068 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005069 finally:
5070 RunGit(['checkout', '-q', branch])
5071 RunGit(['branch', '-D', TMP_BRANCH])
5072
5073 return 0
5074
5075
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005076def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005077 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005078 parser.add_option(
5079 '--no-color',
5080 action='store_true',
5081 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005082 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005083 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005084 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005085
5086 author = RunGit(['config', 'user.email']).strip() or None
5087
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005088 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005089
5090 if args:
5091 if len(args) > 1:
5092 parser.error('Unknown args')
5093 base_branch = args[0]
5094 else:
5095 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005096 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005097
5098 change = cl.GetChange(base_branch, None)
5099 return owners_finder.OwnersFinder(
5100 [f.LocalPath() for f in
5101 cl.GetChange(base_branch, None).AffectedFiles()],
5102 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005103 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005104 disable_color=options.no_color).run()
5105
5106
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005107def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005108 """Generates a diff command."""
5109 # Generate diff for the current branch's changes.
5110 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5111 upstream_commit, '--' ]
5112
5113 if args:
5114 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005115 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005116 diff_cmd.append(arg)
5117 else:
5118 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005119
5120 return diff_cmd
5121
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005122def MatchingFileType(file_name, extensions):
5123 """Returns true if the file name ends with one of the given extensions."""
5124 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005125
enne@chromium.org555cfe42014-01-29 18:21:39 +00005126@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005127def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005128 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005129 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005130 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005131 parser.add_option('--full', action='store_true',
5132 help='Reformat the full content of all touched files')
5133 parser.add_option('--dry-run', action='store_true',
5134 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005135 parser.add_option('--python', action='store_true',
5136 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005137 parser.add_option('--diff', action='store_true',
5138 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005139 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005140
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005141 # git diff generates paths against the root of the repository. Change
5142 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005143 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005144 if rel_base_path:
5145 os.chdir(rel_base_path)
5146
digit@chromium.org29e47272013-05-17 17:01:46 +00005147 # Grab the merge-base commit, i.e. the upstream commit of the current
5148 # branch when it was created or the last time it was rebased. This is
5149 # to cover the case where the user may have called "git fetch origin",
5150 # moving the origin branch to a newer commit, but hasn't rebased yet.
5151 upstream_commit = None
5152 cl = Changelist()
5153 upstream_branch = cl.GetUpstreamBranch()
5154 if upstream_branch:
5155 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5156 upstream_commit = upstream_commit.strip()
5157
5158 if not upstream_commit:
5159 DieWithError('Could not find base commit for this branch. '
5160 'Are you in detached state?')
5161
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005162 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5163 diff_output = RunGit(changed_files_cmd)
5164 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005165 # Filter out files deleted by this CL
5166 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005167
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005168 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5169 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5170 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005171 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005172
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005173 top_dir = os.path.normpath(
5174 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5175
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005176 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5177 # formatted. This is used to block during the presubmit.
5178 return_value = 0
5179
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005180 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005181 # Locate the clang-format binary in the checkout
5182 try:
5183 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005184 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005185 DieWithError(e)
5186
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005187 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005188 cmd = [clang_format_tool]
5189 if not opts.dry_run and not opts.diff:
5190 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005191 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005192 if opts.diff:
5193 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005194 else:
5195 env = os.environ.copy()
5196 env['PATH'] = str(os.path.dirname(clang_format_tool))
5197 try:
5198 script = clang_format.FindClangFormatScriptInChromiumTree(
5199 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005200 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005201 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005202
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005203 cmd = [sys.executable, script, '-p0']
5204 if not opts.dry_run and not opts.diff:
5205 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005206
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005207 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5208 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005209
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005210 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5211 if opts.diff:
5212 sys.stdout.write(stdout)
5213 if opts.dry_run and len(stdout) > 0:
5214 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005215
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005216 # Similar code to above, but using yapf on .py files rather than clang-format
5217 # on C/C++ files
5218 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005219 yapf_tool = gclient_utils.FindExecutable('yapf')
5220 if yapf_tool is None:
5221 DieWithError('yapf not found in PATH')
5222
5223 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005224 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005225 cmd = [yapf_tool]
5226 if not opts.dry_run and not opts.diff:
5227 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005228 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005229 if opts.diff:
5230 sys.stdout.write(stdout)
5231 else:
5232 # TODO(sbc): yapf --lines mode still has some issues.
5233 # https://github.com/google/yapf/issues/154
5234 DieWithError('--python currently only works with --full')
5235
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005236 # Dart's formatter does not have the nice property of only operating on
5237 # modified chunks, so hard code full.
5238 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005239 try:
5240 command = [dart_format.FindDartFmtToolInChromiumTree()]
5241 if not opts.dry_run and not opts.diff:
5242 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005243 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005244
ppi@chromium.org6593d932016-03-03 15:41:15 +00005245 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005246 if opts.dry_run and stdout:
5247 return_value = 2
5248 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005249 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5250 'found in this checkout. Files in other languages are still '
5251 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005252
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005253 # Format GN build files. Always run on full build files for canonical form.
5254 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005255 cmd = ['gn', 'format' ]
5256 if opts.dry_run or opts.diff:
5257 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005258 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005259 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5260 shell=sys.platform == 'win32',
5261 cwd=top_dir)
5262 if opts.dry_run and gn_ret == 2:
5263 return_value = 2 # Not formatted.
5264 elif opts.diff and gn_ret == 2:
5265 # TODO this should compute and print the actual diff.
5266 print("This change has GN build file diff for " + gn_diff_file)
5267 elif gn_ret != 0:
5268 # For non-dry run cases (and non-2 return values for dry-run), a
5269 # nonzero error code indicates a failure, probably because the file
5270 # doesn't parse.
5271 DieWithError("gn format failed on " + gn_diff_file +
5272 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005273
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005274 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005275
5276
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005277@subcommand.usage('<codereview url or issue id>')
5278def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005279 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005280 _, args = parser.parse_args(args)
5281
5282 if len(args) != 1:
5283 parser.print_help()
5284 return 1
5285
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005286 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005287 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005288 parser.print_help()
5289 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005290 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005291
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005292 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005293 output = RunGit(['config', '--local', '--get-regexp',
5294 r'branch\..*\.%s' % issueprefix],
5295 error_ok=True)
5296 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005297 if issue == target_issue:
5298 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005299
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005300 branches = []
5301 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005302 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005303 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005304 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005305 return 1
5306 if len(branches) == 1:
5307 RunGit(['checkout', branches[0]])
5308 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005309 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005310 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005311 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005312 which = raw_input('Choose by index: ')
5313 try:
5314 RunGit(['checkout', branches[int(which)]])
5315 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005316 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005317 return 1
5318
5319 return 0
5320
5321
maruel@chromium.org29404b52014-09-08 22:58:00 +00005322def CMDlol(parser, args):
5323 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005324 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005325 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5326 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5327 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005328 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005329 return 0
5330
5331
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005332class OptionParser(optparse.OptionParser):
5333 """Creates the option parse and add --verbose support."""
5334 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005335 optparse.OptionParser.__init__(
5336 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005337 self.add_option(
5338 '-v', '--verbose', action='count', default=0,
5339 help='Use 2 times for more debugging info')
5340
5341 def parse_args(self, args=None, values=None):
5342 options, args = optparse.OptionParser.parse_args(self, args, values)
5343 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5344 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5345 return options, args
5346
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005347
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005348def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005349 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005350 print('\nYour python version %s is unsupported, please upgrade.\n' %
5351 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005352 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005353
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005354 # Reload settings.
5355 global settings
5356 settings = Settings()
5357
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005358 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005359 dispatcher = subcommand.CommandDispatcher(__name__)
5360 try:
5361 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005362 except auth.AuthenticationError as e:
5363 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005364 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005365 if e.code != 500:
5366 raise
5367 DieWithError(
5368 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5369 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005370 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005371
5372
5373if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005374 # These affect sys.stdout so do it outside of main() to simplify mocks in
5375 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005376 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005377 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005378 try:
5379 sys.exit(main(sys.argv[1:]))
5380 except KeyboardInterrupt:
5381 sys.stderr.write('interrupted\n')
5382 sys.exit(1)