blob: 18dc963407d46321b7f77937af2f9ed1de3fcfeb [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 traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000044import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000045import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000046import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000047import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000048import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000049import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000050import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000052import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000053import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000056import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
tandrii7400cf02016-06-21 08:48:07 -070062__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063
tandrii9d2c7a32016-06-22 03:42:45 -070064COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070065DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070086 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
bsep@chromium.org627d9002016-04-29 00:00:52 +000097def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +000099 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
108
109def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000120 try:
tandrii5d48c322016-08-18 16:19:37 -0700121 (out, _), code = subprocess2.communicate(['git'] + args,
122 env=GetNoGitPagerEnv(),
123 stdout=subprocess2.PIPE,
124 stderr=stderr)
125 return code, out
126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
128 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000132 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
tandrii2a16b952016-10-19 07:09:44 -0700150def time_sleep(seconds):
151 # Use this so that it can be mocked in tests without interfering with python
152 # system machinery.
153 import time # Local import to discourage others from importing time globally.
154 return time.sleep(seconds)
155
156
maruel@chromium.org90541732011-04-01 17:54:18 +0000157def ask_for_data(prompt):
158 try:
159 return raw_input(prompt)
160 except KeyboardInterrupt:
161 # Hide the exception.
162 sys.exit(1)
163
164
tandrii5d48c322016-08-18 16:19:37 -0700165def _git_branch_config_key(branch, key):
166 """Helper method to return Git config key for a branch."""
167 assert branch, 'branch name is required to set git config for it'
168 return 'branch.%s.%s' % (branch, key)
169
170
171def _git_get_branch_config_value(key, default=None, value_type=str,
172 branch=False):
173 """Returns git config value of given or current branch if any.
174
175 Returns default in all other cases.
176 """
177 assert value_type in (int, str, bool)
178 if branch is False: # Distinguishing default arg value from None.
179 branch = GetCurrentBranch()
180
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000181 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700182 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000183
tandrii5d48c322016-08-18 16:19:37 -0700184 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700185 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700186 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700187 # git config also has --int, but apparently git config suffers from integer
188 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700189 args.append(_git_branch_config_key(branch, key))
190 code, out = RunGitWithCode(args)
191 if code == 0:
192 value = out.strip()
193 if value_type == int:
194 return int(value)
195 if value_type == bool:
196 return bool(value.lower() == 'true')
197 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 return default
199
200
tandrii5d48c322016-08-18 16:19:37 -0700201def _git_set_branch_config_value(key, value, branch=None, **kwargs):
202 """Sets the value or unsets if it's None of a git branch config.
203
204 Valid, though not necessarily existing, branch must be provided,
205 otherwise currently checked out branch is used.
206 """
207 if not branch:
208 branch = GetCurrentBranch()
209 assert branch, 'a branch name OR currently checked out branch is required'
210 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700211 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700212 if value is None:
213 args.append('--unset')
214 elif isinstance(value, bool):
215 args.append('--bool')
216 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700217 else:
tandrii33a46ff2016-08-23 05:53:40 -0700218 # git config also has --int, but apparently git config suffers from integer
219 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700220 value = str(value)
221 args.append(_git_branch_config_key(branch, key))
222 if value is not None:
223 args.append(value)
224 RunGit(args, **kwargs)
225
226
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000227def add_git_similarity(parser):
228 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700229 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000230 help='Sets the percentage that a pair of files need to match in order to'
231 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000232 parser.add_option(
233 '--find-copies', action='store_true',
234 help='Allows git to look for copies.')
235 parser.add_option(
236 '--no-find-copies', action='store_false', dest='find_copies',
237 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000238
239 old_parser_args = parser.parse_args
240 def Parse(args):
241 options, args = old_parser_args(args)
242
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000243 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700244 options.similarity = _git_get_branch_config_value(
245 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000246 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247 print('Note: Saving similarity of %d%% in git config.'
248 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700249 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 options.similarity = max(0, min(options.similarity, 100))
252
253 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700254 options.find_copies = _git_get_branch_config_value(
255 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000256 else:
tandrii5d48c322016-08-18 16:19:37 -0700257 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000258
259 print('Using %d%% similarity for rename/copy detection. '
260 'Override with --similarity.' % options.similarity)
261
262 return options, args
263 parser.parse_args = Parse
264
265
machenbach@chromium.org45453142015-09-15 08:45:22 +0000266def _get_properties_from_options(options):
267 properties = dict(x.split('=', 1) for x in options.properties)
268 for key, val in properties.iteritems():
269 try:
270 properties[key] = json.loads(val)
271 except ValueError:
272 pass # If a value couldn't be evaluated, treat it as a string.
273 return properties
274
275
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000276def _prefix_master(master):
277 """Convert user-specified master name to full master name.
278
279 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
280 name, while the developers always use shortened master name
281 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
282 function does the conversion for buildbucket migration.
283 """
284 prefix = 'master.'
285 if master.startswith(prefix):
286 return master
287 return '%s%s' % (prefix, master)
288
289
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000290def _buildbucket_retry(operation_name, http, *args, **kwargs):
291 """Retries requests to buildbucket service and returns parsed json content."""
292 try_count = 0
293 while True:
294 response, content = http.request(*args, **kwargs)
295 try:
296 content_json = json.loads(content)
297 except ValueError:
298 content_json = None
299
300 # Buildbucket could return an error even if status==200.
301 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000302 error = content_json.get('error')
303 if error.get('code') == 403:
304 raise BuildbucketResponseException(
305 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000307 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000308 raise BuildbucketResponseException(msg)
309
310 if response.status == 200:
311 if not content_json:
312 raise BuildbucketResponseException(
313 'Buildbucket returns invalid json content: %s.\n'
314 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
315 content)
316 return content_json
317 if response.status < 500 or try_count >= 2:
318 raise httplib2.HttpLib2Error(content)
319
320 # status >= 500 means transient failures.
321 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700322 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000323 try_count += 1
324 assert False, 'unreachable'
325
326
tandriide281ae2016-10-12 06:02:30 -0700327def _trigger_try_jobs(auth_config, changelist, masters, options,
328 category='git_cl_try', patchset=None):
329 assert changelist.GetIssue(), 'CL must be uploaded first'
330 codereview_url = changelist.GetCodereviewServer()
331 assert codereview_url, 'CL must be uploaded first'
332 patchset = patchset or changelist.GetMostRecentPatchset()
333 assert patchset, 'CL must be uploaded first'
334
335 codereview_host = urlparse.urlparse(codereview_url).hostname
336 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 http = authenticator.authorize(httplib2.Http())
338 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700339
340 # TODO(tandrii): consider caching Gerrit CL details just like
341 # _RietveldChangelistImpl does, then caching values in these two variables
342 # won't be necessary.
343 owner_email = changelist.GetIssueOwner()
344 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345
346 buildbucket_put_url = (
347 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000348 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700349 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
350 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
351 hostname=codereview_host,
352 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700354 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000355
356 batch_req_body = {'builds': []}
357 print_text = []
358 print_text.append('Tried jobs on:')
359 for master, builders_and_tests in sorted(masters.iteritems()):
360 print_text.append('Master: %s' % master)
361 bucket = _prefix_master(master)
362 for builder, tests in sorted(builders_and_tests.iteritems()):
363 print_text.append(' %s: %s' % (builder, tests))
364 parameters = {
365 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000366 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700367 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000368 'revision': options.revision,
369 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370 'properties': {
371 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700372 'issue': changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373 'master': master,
tandriide281ae2016-10-12 06:02:30 -0700374 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000375 'patch_storage': 'rietveld',
376 'patchset': patchset,
377 'reason': options.name,
tandriide281ae2016-10-12 06:02:30 -0700378 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000379 },
380 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000381 if 'presubmit' in builder.lower():
382 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000383 if tests:
384 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700385 if extra_properties:
386 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000387 if options.clobber:
388 parameters['properties']['clobber'] = True
389 batch_req_body['builds'].append(
390 {
391 'bucket': bucket,
392 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000393 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000394 'tags': ['builder:%s' % builder,
395 'buildset:%s' % buildset,
396 'master:%s' % master,
397 'user_agent:git_cl_try']
398 }
399 )
400
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700402 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 http,
404 buildbucket_put_url,
405 'PUT',
406 body=json.dumps(batch_req_body),
407 headers={'Content-Type': 'application/json'}
408 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000409 print_text.append('To see results here, run: git cl try-results')
410 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700411 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000412
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000413
tandrii221ab252016-10-06 08:12:04 -0700414def fetch_try_jobs(auth_config, changelist, buildbucket_host,
415 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700416 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000417
qyearsley53f48a12016-09-01 10:45:13 -0700418 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 """
tandrii221ab252016-10-06 08:12:04 -0700420 assert buildbucket_host
421 assert changelist.GetIssue(), 'CL must be uploaded first'
422 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
423 patchset = patchset or changelist.GetMostRecentPatchset()
424 assert patchset, 'CL must be uploaded first'
425
426 codereview_url = changelist.GetCodereviewServer()
427 codereview_host = urlparse.urlparse(codereview_url).hostname
428 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000429 if authenticator.has_cached_credentials():
430 http = authenticator.authorize(httplib2.Http())
431 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700432 print('Warning: Some results might be missing because %s' %
433 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700434 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000435 http = httplib2.Http()
436
437 http.force_exception_to_status_code = True
438
tandrii221ab252016-10-06 08:12:04 -0700439 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
440 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
441 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000442 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700443 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000444 params = {'tag': 'buildset:%s' % buildset}
445
446 builds = {}
447 while True:
448 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700449 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000450 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700451 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000452 for build in content.get('builds', []):
453 builds[build['id']] = build
454 if 'next_cursor' in content:
455 params['start_cursor'] = content['next_cursor']
456 else:
457 break
458 return builds
459
460
qyearsleyeab3c042016-08-24 09:18:28 -0700461def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462 """Prints nicely result of fetch_try_jobs."""
463 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700464 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000465 return
466
467 # Make a copy, because we'll be modifying builds dictionary.
468 builds = builds.copy()
469 builder_names_cache = {}
470
471 def get_builder(b):
472 try:
473 return builder_names_cache[b['id']]
474 except KeyError:
475 try:
476 parameters = json.loads(b['parameters_json'])
477 name = parameters['builder_name']
478 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700479 print('WARNING: failed to get builder name for build %s: %s' % (
480 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000481 name = None
482 builder_names_cache[b['id']] = name
483 return name
484
485 def get_bucket(b):
486 bucket = b['bucket']
487 if bucket.startswith('master.'):
488 return bucket[len('master.'):]
489 return bucket
490
491 if options.print_master:
492 name_fmt = '%%-%ds %%-%ds' % (
493 max(len(str(get_bucket(b))) for b in builds.itervalues()),
494 max(len(str(get_builder(b))) for b in builds.itervalues()))
495 def get_name(b):
496 return name_fmt % (get_bucket(b), get_builder(b))
497 else:
498 name_fmt = '%%-%ds' % (
499 max(len(str(get_builder(b))) for b in builds.itervalues()))
500 def get_name(b):
501 return name_fmt % get_builder(b)
502
503 def sort_key(b):
504 return b['status'], b.get('result'), get_name(b), b.get('url')
505
506 def pop(title, f, color=None, **kwargs):
507 """Pop matching builds from `builds` dict and print them."""
508
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000509 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 colorize = str
511 else:
512 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
513
514 result = []
515 for b in builds.values():
516 if all(b.get(k) == v for k, v in kwargs.iteritems()):
517 builds.pop(b['id'])
518 result.append(b)
519 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700520 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700522 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000523
524 total = len(builds)
525 pop(status='COMPLETED', result='SUCCESS',
526 title='Successes:', color=Fore.GREEN,
527 f=lambda b: (get_name(b), b.get('url')))
528 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
529 title='Infra Failures:', color=Fore.MAGENTA,
530 f=lambda b: (get_name(b), b.get('url')))
531 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
532 title='Failures:', color=Fore.RED,
533 f=lambda b: (get_name(b), b.get('url')))
534 pop(status='COMPLETED', result='CANCELED',
535 title='Canceled:', color=Fore.MAGENTA,
536 f=lambda b: (get_name(b),))
537 pop(status='COMPLETED', result='FAILURE',
538 failure_reason='INVALID_BUILD_DEFINITION',
539 title='Wrong master/builder name:', color=Fore.MAGENTA,
540 f=lambda b: (get_name(b),))
541 pop(status='COMPLETED', result='FAILURE',
542 title='Other failures:',
543 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
544 pop(status='COMPLETED',
545 title='Other finished:',
546 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
547 pop(status='STARTED',
548 title='Started:', color=Fore.YELLOW,
549 f=lambda b: (get_name(b), b.get('url')))
550 pop(status='SCHEDULED',
551 title='Scheduled:',
552 f=lambda b: (get_name(b), 'id=%s' % b['id']))
553 # The last section is just in case buildbucket API changes OR there is a bug.
554 pop(title='Other:',
555 f=lambda b: (get_name(b), 'id=%s' % b['id']))
556 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700557 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558
559
qyearsley53f48a12016-09-01 10:45:13 -0700560def write_try_results_json(output_file, builds):
561 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
562
563 The input |builds| dict is assumed to be generated by Buildbucket.
564 Buildbucket documentation: http://goo.gl/G0s101
565 """
566
567 def convert_build_dict(build):
568 return {
569 'buildbucket_id': build.get('id'),
570 'status': build.get('status'),
571 'result': build.get('result'),
572 'bucket': build.get('bucket'),
573 'builder_name': json.loads(
574 build.get('parameters_json', '{}')).get('builder_name'),
575 'failure_reason': build.get('failure_reason'),
576 'url': build.get('url'),
577 }
578
579 converted = []
580 for _, build in sorted(builds.items()):
581 converted.append(convert_build_dict(build))
582 write_json(output_file, converted)
583
584
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000585def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
586 """Return the corresponding git ref if |base_url| together with |glob_spec|
587 matches the full |url|.
588
589 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
590 """
591 fetch_suburl, as_ref = glob_spec.split(':')
592 if allow_wildcards:
593 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
594 if glob_match:
595 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
596 # "branches/{472,597,648}/src:refs/remotes/svn/*".
597 branch_re = re.escape(base_url)
598 if glob_match.group(1):
599 branch_re += '/' + re.escape(glob_match.group(1))
600 wildcard = glob_match.group(2)
601 if wildcard == '*':
602 branch_re += '([^/]*)'
603 else:
604 # Escape and replace surrounding braces with parentheses and commas
605 # with pipe symbols.
606 wildcard = re.escape(wildcard)
607 wildcard = re.sub('^\\\\{', '(', wildcard)
608 wildcard = re.sub('\\\\,', '|', wildcard)
609 wildcard = re.sub('\\\\}$', ')', wildcard)
610 branch_re += wildcard
611 if glob_match.group(3):
612 branch_re += re.escape(glob_match.group(3))
613 match = re.match(branch_re, url)
614 if match:
615 return re.sub('\*$', match.group(1), as_ref)
616
617 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
618 if fetch_suburl:
619 full_url = base_url + '/' + fetch_suburl
620 else:
621 full_url = base_url
622 if full_url == url:
623 return as_ref
624 return None
625
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000626
iannucci@chromium.org79540052012-10-19 23:15:26 +0000627def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000628 """Prints statistics about the change to the user."""
629 # --no-ext-diff is broken in some versions of Git, so try to work around
630 # this by overriding the environment (but there is still a problem if the
631 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000633 if 'GIT_EXTERNAL_DIFF' in env:
634 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000635
636 if find_copies:
637 similarity_options = ['--find-copies-harder', '-l100000',
638 '-C%s' % similarity]
639 else:
640 similarity_options = ['-M%s' % similarity]
641
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000642 try:
643 stdout = sys.stdout.fileno()
644 except AttributeError:
645 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000646 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000647 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000648 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000649 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000650
651
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000652class BuildbucketResponseException(Exception):
653 pass
654
655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656class Settings(object):
657 def __init__(self):
658 self.default_server = None
659 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000660 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 self.is_git_svn = None
662 self.svn_branch = None
663 self.tree_status_url = None
664 self.viewvc_url = None
665 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000666 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000667 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000668 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000669 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000670 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000671 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000672 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700673 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674
675 def LazyUpdateIfNeeded(self):
676 """Updates the settings from a codereview.settings file, if available."""
677 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000678 # The only value that actually changes the behavior is
679 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000680 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000681 error_ok=True
682 ).strip().lower()
683
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000685 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 LoadCodereviewSettingsFromFile(cr_settings_file)
687 self.updated = True
688
689 def GetDefaultServerUrl(self, error_ok=False):
690 if not self.default_server:
691 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000692 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000693 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694 if error_ok:
695 return self.default_server
696 if not self.default_server:
697 error_message = ('Could not find settings file. You must configure '
698 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000699 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000700 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000701 return self.default_server
702
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000703 @staticmethod
704 def GetRelativeRoot():
705 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000708 if self.root is None:
709 self.root = os.path.abspath(self.GetRelativeRoot())
710 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000712 def GetGitMirror(self, remote='origin'):
713 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000714 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000715 if not os.path.isdir(local_url):
716 return None
717 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
718 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
719 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
720 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
721 if mirror.exists():
722 return mirror
723 return None
724
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 def GetIsGitSvn(self):
726 """Return true if this repo looks like it's using git-svn."""
727 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000728 if self.GetPendingRefPrefix():
729 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
730 self.is_git_svn = False
731 else:
732 # If you have any "svn-remote.*" config keys, we think you're using svn.
733 self.is_git_svn = RunGitWithCode(
734 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 return self.is_git_svn
736
737 def GetSVNBranch(self):
738 if self.svn_branch is None:
739 if not self.GetIsGitSvn():
740 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
741
742 # Try to figure out which remote branch we're based on.
743 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000744 # 1) iterate through our branch history and find the svn URL.
745 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746
747 # regexp matching the git-svn line that contains the URL.
748 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
749
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000750 # We don't want to go through all of history, so read a line from the
751 # pipe at a time.
752 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000753 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
755 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000756 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000757 for line in proc.stdout:
758 match = git_svn_re.match(line)
759 if match:
760 url = match.group(1)
761 proc.stdout.close() # Cut pipe.
762 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000764 if url:
765 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
766 remotes = RunGit(['config', '--get-regexp',
767 r'^svn-remote\..*\.url']).splitlines()
768 for remote in remotes:
769 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000771 remote = match.group(1)
772 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000773 rewrite_root = RunGit(
774 ['config', 'svn-remote.%s.rewriteRoot' % remote],
775 error_ok=True).strip()
776 if rewrite_root:
777 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000778 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000779 ['config', 'svn-remote.%s.fetch' % remote],
780 error_ok=True).strip()
781 if fetch_spec:
782 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
783 if self.svn_branch:
784 break
785 branch_spec = RunGit(
786 ['config', 'svn-remote.%s.branches' % remote],
787 error_ok=True).strip()
788 if branch_spec:
789 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
790 if self.svn_branch:
791 break
792 tag_spec = RunGit(
793 ['config', 'svn-remote.%s.tags' % remote],
794 error_ok=True).strip()
795 if tag_spec:
796 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
797 if self.svn_branch:
798 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
800 if not self.svn_branch:
801 DieWithError('Can\'t guess svn branch -- try specifying it on the '
802 'command line')
803
804 return self.svn_branch
805
806 def GetTreeStatusUrl(self, error_ok=False):
807 if not self.tree_status_url:
808 error_message = ('You must configure your tree status URL by running '
809 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 self.tree_status_url = self._GetRietveldConfig(
811 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.tree_status_url
813
814 def GetViewVCUrl(self):
815 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000816 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 return self.viewvc_url
818
rmistry@google.com90752582014-01-14 21:04:50 +0000819 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000820 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000821
rmistry@google.com78948ed2015-07-08 23:09:57 +0000822 def GetIsSkipDependencyUpload(self, branch_name):
823 """Returns true if specified branch should skip dep uploads."""
824 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
825 error_ok=True)
826
rmistry@google.com5626a922015-02-26 14:03:30 +0000827 def GetRunPostUploadHook(self):
828 run_post_upload_hook = self._GetRietveldConfig(
829 'run-post-upload-hook', error_ok=True)
830 return run_post_upload_hook == "True"
831
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000832 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000834
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000835 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000837
ukai@chromium.orge8077812012-02-03 03:41:46 +0000838 def GetIsGerrit(self):
839 """Return true if this repo is assosiated with gerrit code review system."""
840 if self.is_gerrit is None:
841 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
842 return self.is_gerrit
843
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000844 def GetSquashGerritUploads(self):
845 """Return true if uploads to Gerrit should be squashed by default."""
846 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700847 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
848 if self.squash_gerrit_uploads is None:
849 # Default is squash now (http://crbug.com/611892#c23).
850 self.squash_gerrit_uploads = not (
851 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
852 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000853 return self.squash_gerrit_uploads
854
tandriia60502f2016-06-20 02:01:53 -0700855 def GetSquashGerritUploadsOverride(self):
856 """Return True or False if codereview.settings should be overridden.
857
858 Returns None if no override has been defined.
859 """
860 # See also http://crbug.com/611892#c23
861 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
862 error_ok=True).strip()
863 if result == 'true':
864 return True
865 if result == 'false':
866 return False
867 return None
868
tandrii@chromium.org28253532016-04-14 13:46:56 +0000869 def GetGerritSkipEnsureAuthenticated(self):
870 """Return True if EnsureAuthenticated should not be done for Gerrit
871 uploads."""
872 if self.gerrit_skip_ensure_authenticated is None:
873 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000874 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000875 error_ok=True).strip() == 'true')
876 return self.gerrit_skip_ensure_authenticated
877
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000878 def GetGitEditor(self):
879 """Return the editor specified in the git config, or None if none is."""
880 if self.git_editor is None:
881 self.git_editor = self._GetConfig('core.editor', error_ok=True)
882 return self.git_editor or None
883
thestig@chromium.org44202a22014-03-11 19:22:18 +0000884 def GetLintRegex(self):
885 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
886 DEFAULT_LINT_REGEX)
887
888 def GetLintIgnoreRegex(self):
889 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
890 DEFAULT_LINT_IGNORE_REGEX)
891
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000892 def GetProject(self):
893 if not self.project:
894 self.project = self._GetRietveldConfig('project', error_ok=True)
895 return self.project
896
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000897 def GetForceHttpsCommitUrl(self):
898 if not self.force_https_commit_url:
899 self.force_https_commit_url = self._GetRietveldConfig(
900 'force-https-commit-url', error_ok=True)
901 return self.force_https_commit_url
902
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000903 def GetPendingRefPrefix(self):
904 if not self.pending_ref_prefix:
905 self.pending_ref_prefix = self._GetRietveldConfig(
906 'pending-ref-prefix', error_ok=True)
907 return self.pending_ref_prefix
908
tandriif46c20f2016-09-14 06:17:05 -0700909 def GetHasGitNumberFooter(self):
910 # TODO(tandrii): this has to be removed after Rietveld is read-only.
911 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
912 if not self.git_number_footer:
913 self.git_number_footer = self._GetRietveldConfig(
914 'git-number-footer', error_ok=True)
915 return self.git_number_footer
916
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000917 def _GetRietveldConfig(self, param, **kwargs):
918 return self._GetConfig('rietveld.' + param, **kwargs)
919
rmistry@google.com78948ed2015-07-08 23:09:57 +0000920 def _GetBranchConfig(self, branch_name, param, **kwargs):
921 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923 def _GetConfig(self, param, **kwargs):
924 self.LazyUpdateIfNeeded()
925 return RunGit(['config', param], **kwargs).strip()
926
927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928def ShortBranchName(branch):
929 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000930 return branch.replace('refs/heads/', '', 1)
931
932
933def GetCurrentBranchRef():
934 """Returns branch ref (e.g., refs/heads/master) or None."""
935 return RunGit(['symbolic-ref', 'HEAD'],
936 stderr=subprocess2.VOID, error_ok=True).strip() or None
937
938
939def GetCurrentBranch():
940 """Returns current branch or None.
941
942 For refs/heads/* branches, returns just last part. For others, full ref.
943 """
944 branchref = GetCurrentBranchRef()
945 if branchref:
946 return ShortBranchName(branchref)
947 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948
949
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000950class _CQState(object):
951 """Enum for states of CL with respect to Commit Queue."""
952 NONE = 'none'
953 DRY_RUN = 'dry_run'
954 COMMIT = 'commit'
955
956 ALL_STATES = [NONE, DRY_RUN, COMMIT]
957
958
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000959class _ParsedIssueNumberArgument(object):
960 def __init__(self, issue=None, patchset=None, hostname=None):
961 self.issue = issue
962 self.patchset = patchset
963 self.hostname = hostname
964
965 @property
966 def valid(self):
967 return self.issue is not None
968
969
970class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
971 def __init__(self, *args, **kwargs):
972 self.patch_url = kwargs.pop('patch_url', None)
973 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
974
975
976def ParseIssueNumberArgument(arg):
977 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
978 fail_result = _ParsedIssueNumberArgument()
979
980 if arg.isdigit():
981 return _ParsedIssueNumberArgument(issue=int(arg))
982 if not arg.startswith('http'):
983 return fail_result
984 url = gclient_utils.UpgradeToHttps(arg)
985 try:
986 parsed_url = urlparse.urlparse(url)
987 except ValueError:
988 return fail_result
989 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
990 tmp = cls.ParseIssueURL(parsed_url)
991 if tmp is not None:
992 return tmp
993 return fail_result
994
995
tandriic2405f52016-10-10 08:13:15 -0700996class GerritIssueNotExists(Exception):
997 def __init__(self, issue, url):
998 self.issue = issue
999 self.url = url
1000 super(GerritIssueNotExists, self).__init__()
1001
1002 def __str__(self):
1003 return 'issue %s at %s does not exist or you have no access to it' % (
1004 self.issue, self.url)
1005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001008 """Changelist works with one changelist in local branch.
1009
1010 Supports two codereview backends: Rietveld or Gerrit, selected at object
1011 creation.
1012
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001013 Notes:
1014 * Not safe for concurrent multi-{thread,process} use.
1015 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001016 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001017 """
1018
1019 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1020 """Create a new ChangeList instance.
1021
1022 If issue is given, the codereview must be given too.
1023
1024 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1025 Otherwise, it's decided based on current configuration of the local branch,
1026 with default being 'rietveld' for backwards compatibility.
1027 See _load_codereview_impl for more details.
1028
1029 **kwargs will be passed directly to codereview implementation.
1030 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001032 global settings
1033 if not settings:
1034 # Happens when git_cl.py is used as a utility library.
1035 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001036
1037 if issue:
1038 assert codereview, 'codereview must be known, if issue is known'
1039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 self.branchref = branchref
1041 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001042 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 self.branch = ShortBranchName(self.branchref)
1044 else:
1045 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001047 self.lookedup_issue = False
1048 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 self.has_description = False
1050 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001051 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001053 self.cc = None
1054 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001055 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001056
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001057 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001058 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001059 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001060 assert self._codereview_impl
1061 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001062
1063 def _load_codereview_impl(self, codereview=None, **kwargs):
1064 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001065 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1066 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1067 self._codereview = codereview
1068 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001069 return
1070
1071 # Automatic selection based on issue number set for a current branch.
1072 # Rietveld takes precedence over Gerrit.
1073 assert not self.issue
1074 # Whether we find issue or not, we are doing the lookup.
1075 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001076 if self.GetBranch():
1077 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1078 issue = _git_get_branch_config_value(
1079 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1080 if issue:
1081 self._codereview = codereview
1082 self._codereview_impl = cls(self, **kwargs)
1083 self.issue = int(issue)
1084 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001085
1086 # No issue is set for this branch, so decide based on repo-wide settings.
1087 return self._load_codereview_impl(
1088 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1089 **kwargs)
1090
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001091 def IsGerrit(self):
1092 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001093
1094 def GetCCList(self):
1095 """Return the users cc'd on this CL.
1096
agable92bec4f2016-08-24 09:27:27 -07001097 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001098 """
1099 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001100 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001101 more_cc = ','.join(self.watchers)
1102 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1103 return self.cc
1104
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001105 def GetCCListWithoutDefault(self):
1106 """Return the users cc'd on this CL excluding default ones."""
1107 if self.cc is None:
1108 self.cc = ','.join(self.watchers)
1109 return self.cc
1110
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001111 def SetWatchers(self, watchers):
1112 """Set the list of email addresses that should be cc'd based on the changed
1113 files in this CL.
1114 """
1115 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116
1117 def GetBranch(self):
1118 """Returns the short branch name, e.g. 'master'."""
1119 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001120 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001121 if not branchref:
1122 return None
1123 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 self.branch = ShortBranchName(self.branchref)
1125 return self.branch
1126
1127 def GetBranchRef(self):
1128 """Returns the full branch name, e.g. 'refs/heads/master'."""
1129 self.GetBranch() # Poke the lazy loader.
1130 return self.branchref
1131
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001132 def ClearBranch(self):
1133 """Clears cached branch data of this object."""
1134 self.branch = self.branchref = None
1135
tandrii5d48c322016-08-18 16:19:37 -07001136 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1137 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1138 kwargs['branch'] = self.GetBranch()
1139 return _git_get_branch_config_value(key, default, **kwargs)
1140
1141 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1142 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1143 assert self.GetBranch(), (
1144 'this CL must have an associated branch to %sset %s%s' %
1145 ('un' if value is None else '',
1146 key,
1147 '' if value is None else ' to %r' % value))
1148 kwargs['branch'] = self.GetBranch()
1149 return _git_set_branch_config_value(key, value, **kwargs)
1150
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001151 @staticmethod
1152 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001153 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 e.g. 'origin', 'refs/heads/master'
1155 """
1156 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001157 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1158
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001160 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001162 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1163 error_ok=True).strip()
1164 if upstream_branch:
1165 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001167 # Fall back on trying a git-svn upstream branch.
1168 if settings.GetIsGitSvn():
1169 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001171 # Else, try to guess the origin remote.
1172 remote_branches = RunGit(['branch', '-r']).split()
1173 if 'origin/master' in remote_branches:
1174 # Fall back on origin/master if it exits.
1175 remote = 'origin'
1176 upstream_branch = 'refs/heads/master'
1177 elif 'origin/trunk' in remote_branches:
1178 # Fall back on origin/trunk if it exists. Generally a shared
1179 # git-svn clone
1180 remote = 'origin'
1181 upstream_branch = 'refs/heads/trunk'
1182 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 DieWithError(
1184 'Unable to determine default branch to diff against.\n'
1185 'Either pass complete "git diff"-style arguments, like\n'
1186 ' git cl upload origin/master\n'
1187 'or verify this branch is set up to track another \n'
1188 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189
1190 return remote, upstream_branch
1191
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001192 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001193 upstream_branch = self.GetUpstreamBranch()
1194 if not BranchExists(upstream_branch):
1195 DieWithError('The upstream for the current branch (%s) does not exist '
1196 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001197 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001198 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200 def GetUpstreamBranch(self):
1201 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001202 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001204 upstream_branch = upstream_branch.replace('refs/heads/',
1205 'refs/remotes/%s/' % remote)
1206 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1207 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 self.upstream_branch = upstream_branch
1209 return self.upstream_branch
1210
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001211 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001212 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001213 remote, branch = None, self.GetBranch()
1214 seen_branches = set()
1215 while branch not in seen_branches:
1216 seen_branches.add(branch)
1217 remote, branch = self.FetchUpstreamTuple(branch)
1218 branch = ShortBranchName(branch)
1219 if remote != '.' or branch.startswith('refs/remotes'):
1220 break
1221 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001222 remotes = RunGit(['remote'], error_ok=True).split()
1223 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001224 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001225 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001226 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001227 logging.warning('Could not determine which remote this change is '
1228 'associated with, so defaulting to "%s". This may '
1229 'not be what you want. You may prevent this message '
1230 'by running "git svn info" as documented here: %s',
1231 self._remote,
1232 GIT_INSTRUCTIONS_URL)
1233 else:
1234 logging.warn('Could not determine which remote this change is '
1235 'associated with. You may prevent this message by '
1236 'running "git svn info" as documented here: %s',
1237 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001238 branch = 'HEAD'
1239 if branch.startswith('refs/remotes'):
1240 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001241 elif branch.startswith('refs/branch-heads/'):
1242 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001243 else:
1244 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001245 return self._remote
1246
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001247 def GitSanityChecks(self, upstream_git_obj):
1248 """Checks git repo status and ensures diff is from local commits."""
1249
sbc@chromium.org79706062015-01-14 21:18:12 +00001250 if upstream_git_obj is None:
1251 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001252 print('ERROR: unable to determine current branch (detached HEAD?)',
1253 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001254 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001255 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001256 return False
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 # Verify the commit we're diffing against is in our current branch.
1259 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1260 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1261 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001262 print('ERROR: %s is not in the current branch. You may need to rebase '
1263 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 return False
1265
1266 # List the commits inside the diff, and verify they are all local.
1267 commits_in_diff = RunGit(
1268 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1269 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1270 remote_branch = remote_branch.strip()
1271 if code != 0:
1272 _, remote_branch = self.GetRemoteBranch()
1273
1274 commits_in_remote = RunGit(
1275 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1276
1277 common_commits = set(commits_in_diff) & set(commits_in_remote)
1278 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001279 print('ERROR: Your diff contains %d commits already in %s.\n'
1280 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1281 'the diff. If you are using a custom git flow, you can override'
1282 ' the reference used for this check with "git config '
1283 'gitcl.remotebranch <git-ref>".' % (
1284 len(common_commits), remote_branch, upstream_git_obj),
1285 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 return False
1287 return True
1288
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001289 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001290 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001291
1292 Returns None if it is not set.
1293 """
tandrii5d48c322016-08-18 16:19:37 -07001294 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001295
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001296 def GetGitSvnRemoteUrl(self):
1297 """Return the configured git-svn remote URL parsed from git svn info.
1298
1299 Returns None if it is not set.
1300 """
1301 # URL is dependent on the current directory.
1302 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1303 if data:
1304 keys = dict(line.split(': ', 1) for line in data.splitlines()
1305 if ': ' in line)
1306 return keys.get('URL', None)
1307 return None
1308
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 def GetRemoteUrl(self):
1310 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1311
1312 Returns None if there is no remote.
1313 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001315 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1316
1317 # If URL is pointing to a local directory, it is probably a git cache.
1318 if os.path.isdir(url):
1319 url = RunGit(['config', 'remote.%s.url' % remote],
1320 error_ok=True,
1321 cwd=url).strip()
1322 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001324 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001325 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001326 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001327 self.issue = self._GitGetBranchConfigValue(
1328 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001329 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 return self.issue
1331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 def GetIssueURL(self):
1333 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001334 issue = self.GetIssue()
1335 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001336 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001337 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338
1339 def GetDescription(self, pretty=False):
1340 if not self.has_description:
1341 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001342 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 self.has_description = True
1344 if pretty:
1345 wrapper = textwrap.TextWrapper()
1346 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1347 return wrapper.fill(self.description)
1348 return self.description
1349
1350 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001351 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001352 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001353 self.patchset = self._GitGetBranchConfigValue(
1354 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001355 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 return self.patchset
1357
1358 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001359 """Set this branch's patchset. If patchset=0, clears the patchset."""
1360 assert self.GetBranch()
1361 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001362 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001363 else:
1364 self.patchset = int(patchset)
1365 self._GitSetBranchConfigValue(
1366 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001368 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001369 """Set this branch's issue. If issue isn't given, clears the issue."""
1370 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001372 issue = int(issue)
1373 self._GitSetBranchConfigValue(
1374 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001375 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001376 codereview_server = self._codereview_impl.GetCodereviewServer()
1377 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001378 self._GitSetBranchConfigValue(
1379 self._codereview_impl.CodereviewServerConfigKey(),
1380 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 else:
tandrii5d48c322016-08-18 16:19:37 -07001382 # Reset all of these just to be clean.
1383 reset_suffixes = [
1384 'last-upload-hash',
1385 self._codereview_impl.IssueConfigKey(),
1386 self._codereview_impl.PatchsetConfigKey(),
1387 self._codereview_impl.CodereviewServerConfigKey(),
1388 ] + self._PostUnsetIssueProperties()
1389 for prop in reset_suffixes:
1390 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001391 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001392 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
dnjba1b0f32016-09-02 12:37:42 -07001394 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001395 if not self.GitSanityChecks(upstream_branch):
1396 DieWithError('\nGit sanity check failure')
1397
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001398 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001399 if not root:
1400 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001401 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001402
1403 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001404 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001405 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001406 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001407 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001408 except subprocess2.CalledProcessError:
1409 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001410 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001411 'This branch probably doesn\'t exist anymore. To reset the\n'
1412 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001413 ' git branch --set-upstream-to origin/master %s\n'
1414 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001415 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001416
maruel@chromium.org52424302012-08-29 15:14:30 +00001417 issue = self.GetIssue()
1418 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001419 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001420 description = self.GetDescription()
1421 else:
1422 # If the change was never uploaded, use the log messages of all commits
1423 # up to the branch point, as git cl upload will prefill the description
1424 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1426 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001427
1428 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001429 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001430 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001431 name,
1432 description,
1433 absroot,
1434 files,
1435 issue,
1436 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001437 author,
1438 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001439
dsansomee2d6fd92016-09-08 00:10:47 -07001440 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001441 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001442 return self._codereview_impl.UpdateDescriptionRemote(
1443 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001444
1445 def RunHook(self, committing, may_prompt, verbose, change):
1446 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1447 try:
1448 return presubmit_support.DoPresubmitChecks(change, committing,
1449 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1450 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001451 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1452 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001453 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001454 DieWithError(
1455 ('%s\nMaybe your depot_tools is out of date?\n'
1456 'If all fails, contact maruel@') % e)
1457
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001458 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1459 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001460 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1461 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001462 else:
1463 # Assume url.
1464 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1465 urlparse.urlparse(issue_arg))
1466 if not parsed_issue_arg or not parsed_issue_arg.valid:
1467 DieWithError('Failed to parse issue argument "%s". '
1468 'Must be an issue number or a valid URL.' % issue_arg)
1469 return self._codereview_impl.CMDPatchWithParsedIssue(
1470 parsed_issue_arg, reject, nocommit, directory)
1471
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001472 def CMDUpload(self, options, git_diff_args, orig_args):
1473 """Uploads a change to codereview."""
1474 if git_diff_args:
1475 # TODO(ukai): is it ok for gerrit case?
1476 base_branch = git_diff_args[0]
1477 else:
1478 if self.GetBranch() is None:
1479 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1480
1481 # Default to diffing against common ancestor of upstream branch
1482 base_branch = self.GetCommonAncestorWithUpstream()
1483 git_diff_args = [base_branch, 'HEAD']
1484
1485 # Make sure authenticated to codereview before running potentially expensive
1486 # hooks. It is a fast, best efforts check. Codereview still can reject the
1487 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001488 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001489
1490 # Apply watchlists on upload.
1491 change = self.GetChange(base_branch, None)
1492 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1493 files = [f.LocalPath() for f in change.AffectedFiles()]
1494 if not options.bypass_watchlists:
1495 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1496
1497 if not options.bypass_hooks:
1498 if options.reviewers or options.tbr_owners:
1499 # Set the reviewer list now so that presubmit checks can access it.
1500 change_description = ChangeDescription(change.FullDescriptionText())
1501 change_description.update_reviewers(options.reviewers,
1502 options.tbr_owners,
1503 change)
1504 change.SetDescriptionText(change_description.description)
1505 hook_results = self.RunHook(committing=False,
1506 may_prompt=not options.force,
1507 verbose=options.verbose,
1508 change=change)
1509 if not hook_results.should_continue():
1510 return 1
1511 if not options.reviewers and hook_results.reviewers:
1512 options.reviewers = hook_results.reviewers.split(',')
1513
1514 if self.GetIssue():
1515 latest_patchset = self.GetMostRecentPatchset()
1516 local_patchset = self.GetPatchset()
1517 if (latest_patchset and local_patchset and
1518 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001519 print('The last upload made from this repository was patchset #%d but '
1520 'the most recent patchset on the server is #%d.'
1521 % (local_patchset, latest_patchset))
1522 print('Uploading will still work, but if you\'ve uploaded to this '
1523 'issue from another machine or branch the patch you\'re '
1524 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001525 ask_for_data('About to upload; enter to confirm.')
1526
1527 print_stats(options.similarity, options.find_copies, git_diff_args)
1528 ret = self.CMDUploadChange(options, git_diff_args, change)
1529 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001530 if options.use_commit_queue:
1531 self.SetCQState(_CQState.COMMIT)
1532 elif options.cq_dry_run:
1533 self.SetCQState(_CQState.DRY_RUN)
1534
tandrii5d48c322016-08-18 16:19:37 -07001535 _git_set_branch_config_value('last-upload-hash',
1536 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001537 # Run post upload hooks, if specified.
1538 if settings.GetRunPostUploadHook():
1539 presubmit_support.DoPostUploadExecuter(
1540 change,
1541 self,
1542 settings.GetRoot(),
1543 options.verbose,
1544 sys.stdout)
1545
1546 # Upload all dependencies if specified.
1547 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001548 print()
1549 print('--dependencies has been specified.')
1550 print('All dependent local branches will be re-uploaded.')
1551 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001552 # Remove the dependencies flag from args so that we do not end up in a
1553 # loop.
1554 orig_args.remove('--dependencies')
1555 ret = upload_branch_deps(self, orig_args)
1556 return ret
1557
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001558 def SetCQState(self, new_state):
1559 """Update the CQ state for latest patchset.
1560
1561 Issue must have been already uploaded and known.
1562 """
1563 assert new_state in _CQState.ALL_STATES
1564 assert self.GetIssue()
1565 return self._codereview_impl.SetCQState(new_state)
1566
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567 # Forward methods to codereview specific implementation.
1568
1569 def CloseIssue(self):
1570 return self._codereview_impl.CloseIssue()
1571
1572 def GetStatus(self):
1573 return self._codereview_impl.GetStatus()
1574
1575 def GetCodereviewServer(self):
1576 return self._codereview_impl.GetCodereviewServer()
1577
tandriide281ae2016-10-12 06:02:30 -07001578 def GetIssueOwner(self):
1579 """Get owner from codereview, which may differ from this checkout."""
1580 return self._codereview_impl.GetIssueOwner()
1581
1582 def GetIssueProject(self):
1583 """Get project from codereview, which may differ from what this
1584 checkout's codereview.settings or gerrit project URL say.
1585 """
1586 return self._codereview_impl.GetIssueProject()
1587
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001588 def GetApprovingReviewers(self):
1589 return self._codereview_impl.GetApprovingReviewers()
1590
1591 def GetMostRecentPatchset(self):
1592 return self._codereview_impl.GetMostRecentPatchset()
1593
tandriide281ae2016-10-12 06:02:30 -07001594 def CannotTriggerTryJobReason(self):
1595 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1596 return self._codereview_impl.CannotTriggerTryJobReason()
1597
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001598 def __getattr__(self, attr):
1599 # This is because lots of untested code accesses Rietveld-specific stuff
1600 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001601 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001602 # Note that child method defines __getattr__ as well, and forwards it here,
1603 # because _RietveldChangelistImpl is not cleaned up yet, and given
1604 # deprecation of Rietveld, it should probably be just removed.
1605 # Until that time, avoid infinite recursion by bypassing __getattr__
1606 # of implementation class.
1607 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608
1609
1610class _ChangelistCodereviewBase(object):
1611 """Abstract base class encapsulating codereview specifics of a changelist."""
1612 def __init__(self, changelist):
1613 self._changelist = changelist # instance of Changelist
1614
1615 def __getattr__(self, attr):
1616 # Forward methods to changelist.
1617 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1618 # _RietveldChangelistImpl to avoid this hack?
1619 return getattr(self._changelist, attr)
1620
1621 def GetStatus(self):
1622 """Apply a rough heuristic to give a simple summary of an issue's review
1623 or CQ status, assuming adherence to a common workflow.
1624
1625 Returns None if no issue for this branch, or specific string keywords.
1626 """
1627 raise NotImplementedError()
1628
1629 def GetCodereviewServer(self):
1630 """Returns server URL without end slash, like "https://codereview.com"."""
1631 raise NotImplementedError()
1632
1633 def FetchDescription(self):
1634 """Fetches and returns description from the codereview server."""
1635 raise NotImplementedError()
1636
tandrii5d48c322016-08-18 16:19:37 -07001637 @classmethod
1638 def IssueConfigKey(cls):
1639 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001640 raise NotImplementedError()
1641
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001642 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001643 def PatchsetConfigKey(cls):
1644 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001645 raise NotImplementedError()
1646
tandrii5d48c322016-08-18 16:19:37 -07001647 @classmethod
1648 def CodereviewServerConfigKey(cls):
1649 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001650 raise NotImplementedError()
1651
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001652 def _PostUnsetIssueProperties(self):
1653 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001654 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001655
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001656 def GetRieveldObjForPresubmit(self):
1657 # This is an unfortunate Rietveld-embeddedness in presubmit.
1658 # For non-Rietveld codereviews, this probably should return a dummy object.
1659 raise NotImplementedError()
1660
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001661 def GetGerritObjForPresubmit(self):
1662 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1663 return None
1664
dsansomee2d6fd92016-09-08 00:10:47 -07001665 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001666 """Update the description on codereview site."""
1667 raise NotImplementedError()
1668
1669 def CloseIssue(self):
1670 """Closes the issue."""
1671 raise NotImplementedError()
1672
1673 def GetApprovingReviewers(self):
1674 """Returns a list of reviewers approving the change.
1675
1676 Note: not necessarily committers.
1677 """
1678 raise NotImplementedError()
1679
1680 def GetMostRecentPatchset(self):
1681 """Returns the most recent patchset number from the codereview site."""
1682 raise NotImplementedError()
1683
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001684 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1685 directory):
1686 """Fetches and applies the issue.
1687
1688 Arguments:
1689 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1690 reject: if True, reject the failed patch instead of switching to 3-way
1691 merge. Rietveld only.
1692 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1693 only.
1694 directory: switch to directory before applying the patch. Rietveld only.
1695 """
1696 raise NotImplementedError()
1697
1698 @staticmethod
1699 def ParseIssueURL(parsed_url):
1700 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1701 failed."""
1702 raise NotImplementedError()
1703
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001704 def EnsureAuthenticated(self, force):
1705 """Best effort check that user is authenticated with codereview server.
1706
1707 Arguments:
1708 force: whether to skip confirmation questions.
1709 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001710 raise NotImplementedError()
1711
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001712 def CMDUploadChange(self, options, args, change):
1713 """Uploads a change to codereview."""
1714 raise NotImplementedError()
1715
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001716 def SetCQState(self, new_state):
1717 """Update the CQ state for latest patchset.
1718
1719 Issue must have been already uploaded and known.
1720 """
1721 raise NotImplementedError()
1722
tandriie113dfd2016-10-11 10:20:12 -07001723 def CannotTriggerTryJobReason(self):
1724 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1725 raise NotImplementedError()
1726
tandriide281ae2016-10-12 06:02:30 -07001727 def GetIssueOwner(self):
1728 raise NotImplementedError()
1729
1730 def GetIssueProject(self):
1731 raise NotImplementedError()
1732
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001733
1734class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1735 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1736 super(_RietveldChangelistImpl, self).__init__(changelist)
1737 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001738 if not rietveld_server:
1739 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740
1741 self._rietveld_server = rietveld_server
1742 self._auth_config = auth_config
1743 self._props = None
1744 self._rpc_server = None
1745
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 def GetCodereviewServer(self):
1747 if not self._rietveld_server:
1748 # If we're on a branch then get the server potentially associated
1749 # with that branch.
1750 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001751 self._rietveld_server = gclient_utils.UpgradeToHttps(
1752 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 if not self._rietveld_server:
1754 self._rietveld_server = settings.GetDefaultServerUrl()
1755 return self._rietveld_server
1756
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001757 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001758 """Best effort check that user is authenticated with Rietveld server."""
1759 if self._auth_config.use_oauth2:
1760 authenticator = auth.get_authenticator_for_host(
1761 self.GetCodereviewServer(), self._auth_config)
1762 if not authenticator.has_cached_credentials():
1763 raise auth.LoginRequiredError(self.GetCodereviewServer())
1764
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765 def FetchDescription(self):
1766 issue = self.GetIssue()
1767 assert issue
1768 try:
1769 return self.RpcServer().get_description(issue).strip()
1770 except urllib2.HTTPError as e:
1771 if e.code == 404:
1772 DieWithError(
1773 ('\nWhile fetching the description for issue %d, received a '
1774 '404 (not found)\n'
1775 'error. It is likely that you deleted this '
1776 'issue on the server. If this is the\n'
1777 'case, please run\n\n'
1778 ' git cl issue 0\n\n'
1779 'to clear the association with the deleted issue. Then run '
1780 'this command again.') % issue)
1781 else:
1782 DieWithError(
1783 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1784 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001785 print('Warning: Failed to retrieve CL description due to network '
1786 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001787 return ''
1788
1789 def GetMostRecentPatchset(self):
1790 return self.GetIssueProperties()['patchsets'][-1]
1791
1792 def GetPatchSetDiff(self, issue, patchset):
1793 return self.RpcServer().get(
1794 '/download/issue%s_%s.diff' % (issue, patchset))
1795
1796 def GetIssueProperties(self):
1797 if self._props is None:
1798 issue = self.GetIssue()
1799 if not issue:
1800 self._props = {}
1801 else:
1802 self._props = self.RpcServer().get_issue_properties(issue, True)
1803 return self._props
1804
tandriie113dfd2016-10-11 10:20:12 -07001805 def CannotTriggerTryJobReason(self):
1806 props = self.GetIssueProperties()
1807 if not props:
1808 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1809 if props.get('closed'):
1810 return 'CL %s is closed' % self.GetIssue()
1811 if props.get('private'):
1812 return 'CL %s is private' % self.GetIssue()
1813 return None
1814
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 def GetApprovingReviewers(self):
1816 return get_approving_reviewers(self.GetIssueProperties())
1817
tandriide281ae2016-10-12 06:02:30 -07001818 def GetIssueOwner(self):
1819 return (self.GetIssueProperties() or {}).get('owner_email')
1820
1821 def GetIssueProject(self):
1822 return (self.GetIssueProperties() or {}).get('project')
1823
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 def AddComment(self, message):
1825 return self.RpcServer().add_comment(self.GetIssue(), message)
1826
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001827 def GetStatus(self):
1828 """Apply a rough heuristic to give a simple summary of an issue's review
1829 or CQ status, assuming adherence to a common workflow.
1830
1831 Returns None if no issue for this branch, or one of the following keywords:
1832 * 'error' - error from review tool (including deleted issues)
1833 * 'unsent' - not sent for review
1834 * 'waiting' - waiting for review
1835 * 'reply' - waiting for owner to reply to review
1836 * 'lgtm' - LGTM from at least one approved reviewer
1837 * 'commit' - in the commit queue
1838 * 'closed' - closed
1839 """
1840 if not self.GetIssue():
1841 return None
1842
1843 try:
1844 props = self.GetIssueProperties()
1845 except urllib2.HTTPError:
1846 return 'error'
1847
1848 if props.get('closed'):
1849 # Issue is closed.
1850 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001851 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001852 # Issue is in the commit queue.
1853 return 'commit'
1854
1855 try:
1856 reviewers = self.GetApprovingReviewers()
1857 except urllib2.HTTPError:
1858 return 'error'
1859
1860 if reviewers:
1861 # Was LGTM'ed.
1862 return 'lgtm'
1863
1864 messages = props.get('messages') or []
1865
tandrii9d2c7a32016-06-22 03:42:45 -07001866 # Skip CQ messages that don't require owner's action.
1867 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1868 if 'Dry run:' in messages[-1]['text']:
1869 messages.pop()
1870 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1871 # This message always follows prior messages from CQ,
1872 # so skip this too.
1873 messages.pop()
1874 else:
1875 # This is probably a CQ messages warranting user attention.
1876 break
1877
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001878 if not messages:
1879 # No message was sent.
1880 return 'unsent'
1881 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001882 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001883 return 'reply'
1884 return 'waiting'
1885
dsansomee2d6fd92016-09-08 00:10:47 -07001886 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001887 return self.RpcServer().update_description(
1888 self.GetIssue(), self.description)
1889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001891 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001893 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001894 return self.SetFlags({flag: value})
1895
1896 def SetFlags(self, flags):
1897 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001898 """
phajdan.jr68598232016-08-10 03:28:28 -07001899 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001900 try:
tandrii4b233bd2016-07-06 03:50:29 -07001901 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001902 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001903 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001904 if e.code == 404:
1905 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1906 if e.code == 403:
1907 DieWithError(
1908 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001909 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001910 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001912 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 """Returns an upload.RpcServer() to access this review's rietveld instance.
1914 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001915 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001916 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001917 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001918 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001919 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001921 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001922 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001923 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924
tandrii5d48c322016-08-18 16:19:37 -07001925 @classmethod
1926 def PatchsetConfigKey(cls):
1927 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928
tandrii5d48c322016-08-18 16:19:37 -07001929 @classmethod
1930 def CodereviewServerConfigKey(cls):
1931 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001933 def GetRieveldObjForPresubmit(self):
1934 return self.RpcServer()
1935
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001936 def SetCQState(self, new_state):
1937 props = self.GetIssueProperties()
1938 if props.get('private'):
1939 DieWithError('Cannot set-commit on private issue')
1940
1941 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001942 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001943 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001944 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001945 else:
tandrii4b233bd2016-07-06 03:50:29 -07001946 assert new_state == _CQState.DRY_RUN
1947 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001948
1949
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001950 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1951 directory):
1952 # TODO(maruel): Use apply_issue.py
1953
1954 # PatchIssue should never be called with a dirty tree. It is up to the
1955 # caller to check this, but just in case we assert here since the
1956 # consequences of the caller not checking this could be dire.
1957 assert(not git_common.is_dirty_git_tree('apply'))
1958 assert(parsed_issue_arg.valid)
1959 self._changelist.issue = parsed_issue_arg.issue
1960 if parsed_issue_arg.hostname:
1961 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1962
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001963 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1964 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001965 assert parsed_issue_arg.patchset
1966 patchset = parsed_issue_arg.patchset
1967 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1968 else:
1969 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1970 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1971
1972 # Switch up to the top-level directory, if necessary, in preparation for
1973 # applying the patch.
1974 top = settings.GetRelativeRoot()
1975 if top:
1976 os.chdir(top)
1977
1978 # Git patches have a/ at the beginning of source paths. We strip that out
1979 # with a sed script rather than the -p flag to patch so we can feed either
1980 # Git or svn-style patches into the same apply command.
1981 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1982 try:
1983 patch_data = subprocess2.check_output(
1984 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1985 except subprocess2.CalledProcessError:
1986 DieWithError('Git patch mungling failed.')
1987 logging.info(patch_data)
1988
1989 # We use "git apply" to apply the patch instead of "patch" so that we can
1990 # pick up file adds.
1991 # The --index flag means: also insert into the index (so we catch adds).
1992 cmd = ['git', 'apply', '--index', '-p0']
1993 if directory:
1994 cmd.extend(('--directory', directory))
1995 if reject:
1996 cmd.append('--reject')
1997 elif IsGitVersionAtLeast('1.7.12'):
1998 cmd.append('--3way')
1999 try:
2000 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
2001 stdin=patch_data, stdout=subprocess2.VOID)
2002 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07002003 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002004 return 1
2005
2006 # If we had an issue, commit the current state and register the issue.
2007 if not nocommit:
2008 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2009 'patch from issue %(i)s at patchset '
2010 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2011 % {'i': self.GetIssue(), 'p': patchset})])
2012 self.SetIssue(self.GetIssue())
2013 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002014 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002015 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002016 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002017 return 0
2018
2019 @staticmethod
2020 def ParseIssueURL(parsed_url):
2021 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2022 return None
wychen3c1c1722016-08-04 11:46:36 -07002023 # Rietveld patch: https://domain/<number>/#ps<patchset>
2024 match = re.match(r'/(\d+)/$', parsed_url.path)
2025 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2026 if match and match2:
2027 return _RietveldParsedIssueNumberArgument(
2028 issue=int(match.group(1)),
2029 patchset=int(match2.group(1)),
2030 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002031 # Typical url: https://domain/<issue_number>[/[other]]
2032 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2033 if match:
2034 return _RietveldParsedIssueNumberArgument(
2035 issue=int(match.group(1)),
2036 hostname=parsed_url.netloc)
2037 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2038 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2039 if match:
2040 return _RietveldParsedIssueNumberArgument(
2041 issue=int(match.group(1)),
2042 patchset=int(match.group(2)),
2043 hostname=parsed_url.netloc,
2044 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
2045 return None
2046
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002047 def CMDUploadChange(self, options, args, change):
2048 """Upload the patch to Rietveld."""
2049 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2050 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002051 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2052 if options.emulate_svn_auto_props:
2053 upload_args.append('--emulate_svn_auto_props')
2054
2055 change_desc = None
2056
2057 if options.email is not None:
2058 upload_args.extend(['--email', options.email])
2059
2060 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002061 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002062 upload_args.extend(['--title', options.title])
2063 if options.message:
2064 upload_args.extend(['--message', options.message])
2065 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002066 print('This branch is associated with issue %s. '
2067 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002068 else:
nodirca166002016-06-27 10:59:51 -07002069 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002070 upload_args.extend(['--title', options.title])
2071 message = (options.title or options.message or
2072 CreateDescriptionFromLog(args))
2073 change_desc = ChangeDescription(message)
2074 if options.reviewers or options.tbr_owners:
2075 change_desc.update_reviewers(options.reviewers,
2076 options.tbr_owners,
2077 change)
2078 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002079 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002080
2081 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002082 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002083 return 1
2084
2085 upload_args.extend(['--message', change_desc.description])
2086 if change_desc.get_reviewers():
2087 upload_args.append('--reviewers=%s' % ','.join(
2088 change_desc.get_reviewers()))
2089 if options.send_mail:
2090 if not change_desc.get_reviewers():
2091 DieWithError("Must specify reviewers to send email.")
2092 upload_args.append('--send_mail')
2093
2094 # We check this before applying rietveld.private assuming that in
2095 # rietveld.cc only addresses which we can send private CLs to are listed
2096 # if rietveld.private is set, and so we should ignore rietveld.cc only
2097 # when --private is specified explicitly on the command line.
2098 if options.private:
2099 logging.warn('rietveld.cc is ignored since private flag is specified. '
2100 'You need to review and add them manually if necessary.')
2101 cc = self.GetCCListWithoutDefault()
2102 else:
2103 cc = self.GetCCList()
2104 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2105 if cc:
2106 upload_args.extend(['--cc', cc])
2107
2108 if options.private or settings.GetDefaultPrivateFlag() == "True":
2109 upload_args.append('--private')
2110
2111 upload_args.extend(['--git_similarity', str(options.similarity)])
2112 if not options.find_copies:
2113 upload_args.extend(['--git_no_find_copies'])
2114
2115 # Include the upstream repo's URL in the change -- this is useful for
2116 # projects that have their source spread across multiple repos.
2117 remote_url = self.GetGitBaseUrlFromConfig()
2118 if not remote_url:
2119 if settings.GetIsGitSvn():
2120 remote_url = self.GetGitSvnRemoteUrl()
2121 else:
2122 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2123 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2124 self.GetUpstreamBranch().split('/')[-1])
2125 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002126 remote, remote_branch = self.GetRemoteBranch()
2127 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2128 settings.GetPendingRefPrefix())
2129 if target_ref:
2130 upload_args.extend(['--target_ref', target_ref])
2131
2132 # Look for dependent patchsets. See crbug.com/480453 for more details.
2133 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2134 upstream_branch = ShortBranchName(upstream_branch)
2135 if remote is '.':
2136 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002137 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002138 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002139 print()
2140 print('Skipping dependency patchset upload because git config '
2141 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2142 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 else:
2144 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002145 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146 auth_config=auth_config)
2147 branch_cl_issue_url = branch_cl.GetIssueURL()
2148 branch_cl_issue = branch_cl.GetIssue()
2149 branch_cl_patchset = branch_cl.GetPatchset()
2150 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2151 upload_args.extend(
2152 ['--depends_on_patchset', '%s:%s' % (
2153 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002154 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155 '\n'
2156 'The current branch (%s) is tracking a local branch (%s) with '
2157 'an associated CL.\n'
2158 'Adding %s/#ps%s as a dependency patchset.\n'
2159 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2160 branch_cl_patchset))
2161
2162 project = settings.GetProject()
2163 if project:
2164 upload_args.extend(['--project', project])
2165
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 try:
2167 upload_args = ['upload'] + upload_args + args
2168 logging.info('upload.RealMain(%s)', upload_args)
2169 issue, patchset = upload.RealMain(upload_args)
2170 issue = int(issue)
2171 patchset = int(patchset)
2172 except KeyboardInterrupt:
2173 sys.exit(1)
2174 except:
2175 # If we got an exception after the user typed a description for their
2176 # change, back up the description before re-raising.
2177 if change_desc:
2178 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2179 print('\nGot exception while uploading -- saving description to %s\n' %
2180 backup_path)
2181 backup_file = open(backup_path, 'w')
2182 backup_file.write(change_desc.description)
2183 backup_file.close()
2184 raise
2185
2186 if not self.GetIssue():
2187 self.SetIssue(issue)
2188 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189 return 0
2190
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002191
2192class _GerritChangelistImpl(_ChangelistCodereviewBase):
2193 def __init__(self, changelist, auth_config=None):
2194 # auth_config is Rietveld thing, kept here to preserve interface only.
2195 super(_GerritChangelistImpl, self).__init__(changelist)
2196 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002197 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002198 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002199 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002200
2201 def _GetGerritHost(self):
2202 # Lazy load of configs.
2203 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002204 if self._gerrit_host and '.' not in self._gerrit_host:
2205 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2206 # This happens for internal stuff http://crbug.com/614312.
2207 parsed = urlparse.urlparse(self.GetRemoteUrl())
2208 if parsed.scheme == 'sso':
2209 print('WARNING: using non https URLs for remote is likely broken\n'
2210 ' Your current remote is: %s' % self.GetRemoteUrl())
2211 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2212 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002213 return self._gerrit_host
2214
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002215 def _GetGitHost(self):
2216 """Returns git host to be used when uploading change to Gerrit."""
2217 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2218
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002219 def GetCodereviewServer(self):
2220 if not self._gerrit_server:
2221 # If we're on a branch then get the server potentially associated
2222 # with that branch.
2223 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002224 self._gerrit_server = self._GitGetBranchConfigValue(
2225 self.CodereviewServerConfigKey())
2226 if self._gerrit_server:
2227 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002228 if not self._gerrit_server:
2229 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2230 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002231 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002232 parts[0] = parts[0] + '-review'
2233 self._gerrit_host = '.'.join(parts)
2234 self._gerrit_server = 'https://%s' % self._gerrit_host
2235 return self._gerrit_server
2236
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002237 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002238 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002239 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002240
tandrii5d48c322016-08-18 16:19:37 -07002241 @classmethod
2242 def PatchsetConfigKey(cls):
2243 return 'gerritpatchset'
2244
2245 @classmethod
2246 def CodereviewServerConfigKey(cls):
2247 return 'gerritserver'
2248
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002249 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002250 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002251 if settings.GetGerritSkipEnsureAuthenticated():
2252 # For projects with unusual authentication schemes.
2253 # See http://crbug.com/603378.
2254 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002255 # Lazy-loader to identify Gerrit and Git hosts.
2256 if gerrit_util.GceAuthenticator.is_gce():
2257 return
2258 self.GetCodereviewServer()
2259 git_host = self._GetGitHost()
2260 assert self._gerrit_server and self._gerrit_host
2261 cookie_auth = gerrit_util.CookiesAuthenticator()
2262
2263 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2264 git_auth = cookie_auth.get_auth_header(git_host)
2265 if gerrit_auth and git_auth:
2266 if gerrit_auth == git_auth:
2267 return
2268 print((
2269 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2270 ' Check your %s or %s file for credentials of hosts:\n'
2271 ' %s\n'
2272 ' %s\n'
2273 ' %s') %
2274 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2275 git_host, self._gerrit_host,
2276 cookie_auth.get_new_password_message(git_host)))
2277 if not force:
2278 ask_for_data('If you know what you are doing, press Enter to continue, '
2279 'Ctrl+C to abort.')
2280 return
2281 else:
2282 missing = (
2283 [] if gerrit_auth else [self._gerrit_host] +
2284 [] if git_auth else [git_host])
2285 DieWithError('Credentials for the following hosts are required:\n'
2286 ' %s\n'
2287 'These are read from %s (or legacy %s)\n'
2288 '%s' % (
2289 '\n '.join(missing),
2290 cookie_auth.get_gitcookies_path(),
2291 cookie_auth.get_netrc_path(),
2292 cookie_auth.get_new_password_message(git_host)))
2293
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002294 def _PostUnsetIssueProperties(self):
2295 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002296 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002297
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002298 def GetRieveldObjForPresubmit(self):
2299 class ThisIsNotRietveldIssue(object):
2300 def __nonzero__(self):
2301 # This is a hack to make presubmit_support think that rietveld is not
2302 # defined, yet still ensure that calls directly result in a decent
2303 # exception message below.
2304 return False
2305
2306 def __getattr__(self, attr):
2307 print(
2308 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2309 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2310 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2311 'or use Rietveld for codereview.\n'
2312 'See also http://crbug.com/579160.' % attr)
2313 raise NotImplementedError()
2314 return ThisIsNotRietveldIssue()
2315
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002316 def GetGerritObjForPresubmit(self):
2317 return presubmit_support.GerritAccessor(self._GetGerritHost())
2318
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002319 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002320 """Apply a rough heuristic to give a simple summary of an issue's review
2321 or CQ status, assuming adherence to a common workflow.
2322
2323 Returns None if no issue for this branch, or one of the following keywords:
2324 * 'error' - error from review tool (including deleted issues)
2325 * 'unsent' - no reviewers added
2326 * 'waiting' - waiting for review
2327 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002328 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2329 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002330 * 'commit' - in the commit queue
2331 * 'closed' - abandoned
2332 """
2333 if not self.GetIssue():
2334 return None
2335
2336 try:
2337 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002338 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002339 return 'error'
2340
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002341 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002342 return 'closed'
2343
2344 cq_label = data['labels'].get('Commit-Queue', {})
2345 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002346 votes = cq_label.get('all', [])
2347 highest_vote = 0
2348 for v in votes:
2349 highest_vote = max(highest_vote, v.get('value', 0))
2350 vote_value = str(highest_vote)
2351 if vote_value != '0':
2352 # Add a '+' if the value is not 0 to match the values in the label.
2353 # The cq_label does not have negatives.
2354 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002355 vote_text = cq_label.get('values', {}).get(vote_value, '')
2356 if vote_text.lower() == 'commit':
2357 return 'commit'
2358
2359 lgtm_label = data['labels'].get('Code-Review', {})
2360 if lgtm_label:
2361 if 'rejected' in lgtm_label:
2362 return 'not lgtm'
2363 if 'approved' in lgtm_label:
2364 return 'lgtm'
2365
2366 if not data.get('reviewers', {}).get('REVIEWER', []):
2367 return 'unsent'
2368
2369 messages = data.get('messages', [])
2370 if messages:
2371 owner = data['owner'].get('_account_id')
2372 last_message_author = messages[-1].get('author', {}).get('_account_id')
2373 if owner != last_message_author:
2374 # Some reply from non-owner.
2375 return 'reply'
2376
2377 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002378
2379 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002380 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381 return data['revisions'][data['current_revision']]['_number']
2382
2383 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002384 data = self._GetChangeDetail(['CURRENT_REVISION'])
2385 current_rev = data['current_revision']
2386 url = data['revisions'][current_rev]['fetch']['http']['url']
2387 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002388
dsansomee2d6fd92016-09-08 00:10:47 -07002389 def UpdateDescriptionRemote(self, description, force=False):
2390 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2391 if not force:
2392 ask_for_data(
2393 'The description cannot be modified while the issue has a pending '
2394 'unpublished edit. Either publish the edit in the Gerrit web UI '
2395 'or delete it.\n\n'
2396 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2397
2398 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2399 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002400 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2401 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002402
2403 def CloseIssue(self):
2404 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2405
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002406 def GetApprovingReviewers(self):
2407 """Returns a list of reviewers approving the change.
2408
2409 Note: not necessarily committers.
2410 """
2411 raise NotImplementedError()
2412
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002413 def SubmitIssue(self, wait_for_merge=True):
2414 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2415 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002416
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002417 def _GetChangeDetail(self, options=None, issue=None):
2418 options = options or []
2419 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002420 assert issue, 'issue is required to query Gerrit'
2421 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002422 options)
tandriic2405f52016-10-10 08:13:15 -07002423 if not data:
2424 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2425 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002426
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002427 def CMDLand(self, force, bypass_hooks, verbose):
2428 if git_common.is_dirty_git_tree('land'):
2429 return 1
tandriid60367b2016-06-22 05:25:12 -07002430 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2431 if u'Commit-Queue' in detail.get('labels', {}):
2432 if not force:
2433 ask_for_data('\nIt seems this repository has a Commit Queue, '
2434 'which can test and land changes for you. '
2435 'Are you sure you wish to bypass it?\n'
2436 'Press Enter to continue, Ctrl+C to abort.')
2437
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002438 differs = True
tandriic4344b52016-08-29 06:04:54 -07002439 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002440 # Note: git diff outputs nothing if there is no diff.
2441 if not last_upload or RunGit(['diff', last_upload]).strip():
2442 print('WARNING: some changes from local branch haven\'t been uploaded')
2443 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002444 if detail['current_revision'] == last_upload:
2445 differs = False
2446 else:
2447 print('WARNING: local branch contents differ from latest uploaded '
2448 'patchset')
2449 if differs:
2450 if not force:
2451 ask_for_data(
2452 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2453 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2454 elif not bypass_hooks:
2455 hook_results = self.RunHook(
2456 committing=True,
2457 may_prompt=not force,
2458 verbose=verbose,
2459 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2460 if not hook_results.should_continue():
2461 return 1
2462
2463 self.SubmitIssue(wait_for_merge=True)
2464 print('Issue %s has been submitted.' % self.GetIssueURL())
2465 return 0
2466
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002467 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2468 directory):
2469 assert not reject
2470 assert not nocommit
2471 assert not directory
2472 assert parsed_issue_arg.valid
2473
2474 self._changelist.issue = parsed_issue_arg.issue
2475
2476 if parsed_issue_arg.hostname:
2477 self._gerrit_host = parsed_issue_arg.hostname
2478 self._gerrit_server = 'https://%s' % self._gerrit_host
2479
tandriic2405f52016-10-10 08:13:15 -07002480 try:
2481 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2482 except GerritIssueNotExists as e:
2483 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002484
2485 if not parsed_issue_arg.patchset:
2486 # Use current revision by default.
2487 revision_info = detail['revisions'][detail['current_revision']]
2488 patchset = int(revision_info['_number'])
2489 else:
2490 patchset = parsed_issue_arg.patchset
2491 for revision_info in detail['revisions'].itervalues():
2492 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2493 break
2494 else:
2495 DieWithError('Couldn\'t find patchset %i in issue %i' %
2496 (parsed_issue_arg.patchset, self.GetIssue()))
2497
2498 fetch_info = revision_info['fetch']['http']
2499 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2500 RunGit(['cherry-pick', 'FETCH_HEAD'])
2501 self.SetIssue(self.GetIssue())
2502 self.SetPatchset(patchset)
2503 print('Committed patch for issue %i pathset %i locally' %
2504 (self.GetIssue(), self.GetPatchset()))
2505 return 0
2506
2507 @staticmethod
2508 def ParseIssueURL(parsed_url):
2509 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2510 return None
2511 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2512 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2513 # Short urls like https://domain/<issue_number> can be used, but don't allow
2514 # specifying the patchset (you'd 404), but we allow that here.
2515 if parsed_url.path == '/':
2516 part = parsed_url.fragment
2517 else:
2518 part = parsed_url.path
2519 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2520 if match:
2521 return _ParsedIssueNumberArgument(
2522 issue=int(match.group(2)),
2523 patchset=int(match.group(4)) if match.group(4) else None,
2524 hostname=parsed_url.netloc)
2525 return None
2526
tandrii16e0b4e2016-06-07 10:34:28 -07002527 def _GerritCommitMsgHookCheck(self, offer_removal):
2528 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2529 if not os.path.exists(hook):
2530 return
2531 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2532 # custom developer made one.
2533 data = gclient_utils.FileRead(hook)
2534 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2535 return
2536 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002537 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002538 'and may interfere with it in subtle ways.\n'
2539 'We recommend you remove the commit-msg hook.')
2540 if offer_removal:
2541 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2542 if reply.lower().startswith('y'):
2543 gclient_utils.rm_file_or_tree(hook)
2544 print('Gerrit commit-msg hook removed.')
2545 else:
2546 print('OK, will keep Gerrit commit-msg hook in place.')
2547
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002548 def CMDUploadChange(self, options, args, change):
2549 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002550 if options.squash and options.no_squash:
2551 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002552
2553 if not options.squash and not options.no_squash:
2554 # Load default for user, repo, squash=true, in this order.
2555 options.squash = settings.GetSquashGerritUploads()
2556 elif options.no_squash:
2557 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002558
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002559 # We assume the remote called "origin" is the one we want.
2560 # It is probably not worthwhile to support different workflows.
2561 gerrit_remote = 'origin'
2562
2563 remote, remote_branch = self.GetRemoteBranch()
2564 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2565 pending_prefix='')
2566
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002567 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002568 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002569 if self.GetIssue():
2570 # Try to get the message from a previous upload.
2571 message = self.GetDescription()
2572 if not message:
2573 DieWithError(
2574 'failed to fetch description from current Gerrit issue %d\n'
2575 '%s' % (self.GetIssue(), self.GetIssueURL()))
2576 change_id = self._GetChangeDetail()['change_id']
2577 while True:
2578 footer_change_ids = git_footers.get_footer_change_id(message)
2579 if footer_change_ids == [change_id]:
2580 break
2581 if not footer_change_ids:
2582 message = git_footers.add_footer_change_id(message, change_id)
2583 print('WARNING: appended missing Change-Id to issue description')
2584 continue
2585 # There is already a valid footer but with different or several ids.
2586 # Doing this automatically is non-trivial as we don't want to lose
2587 # existing other footers, yet we want to append just 1 desired
2588 # Change-Id. Thus, just create a new footer, but let user verify the
2589 # new description.
2590 message = '%s\n\nChange-Id: %s' % (message, change_id)
2591 print(
2592 'WARNING: issue %s has Change-Id footer(s):\n'
2593 ' %s\n'
2594 'but issue has Change-Id %s, according to Gerrit.\n'
2595 'Please, check the proposed correction to the description, '
2596 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2597 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2598 change_id))
2599 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2600 if not options.force:
2601 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002602 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002603 message = change_desc.description
2604 if not message:
2605 DieWithError("Description is empty. Aborting...")
2606 # Continue the while loop.
2607 # Sanity check of this code - we should end up with proper message
2608 # footer.
2609 assert [change_id] == git_footers.get_footer_change_id(message)
2610 change_desc = ChangeDescription(message)
2611 else:
2612 change_desc = ChangeDescription(
2613 options.message or CreateDescriptionFromLog(args))
2614 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002615 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002616 if not change_desc.description:
2617 DieWithError("Description is empty. Aborting...")
2618 message = change_desc.description
2619 change_ids = git_footers.get_footer_change_id(message)
2620 if len(change_ids) > 1:
2621 DieWithError('too many Change-Id footers, at most 1 allowed.')
2622 if not change_ids:
2623 # Generate the Change-Id automatically.
2624 message = git_footers.add_footer_change_id(
2625 message, GenerateGerritChangeId(message))
2626 change_desc.set_description(message)
2627 change_ids = git_footers.get_footer_change_id(message)
2628 assert len(change_ids) == 1
2629 change_id = change_ids[0]
2630
2631 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2632 if remote is '.':
2633 # If our upstream branch is local, we base our squashed commit on its
2634 # squashed version.
2635 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2636 # Check the squashed hash of the parent.
2637 parent = RunGit(['config',
2638 'branch.%s.gerritsquashhash' % upstream_branch_name],
2639 error_ok=True).strip()
2640 # Verify that the upstream branch has been uploaded too, otherwise
2641 # Gerrit will create additional CLs when uploading.
2642 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2643 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 DieWithError(
2645 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002646 'Note: maybe you\'ve uploaded it with --no-squash. '
2647 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 ' git cl upload --squash\n' % upstream_branch_name)
2649 else:
2650 parent = self.GetCommonAncestorWithUpstream()
2651
2652 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2653 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2654 '-m', message]).strip()
2655 else:
2656 change_desc = ChangeDescription(
2657 options.message or CreateDescriptionFromLog(args))
2658 if not change_desc.description:
2659 DieWithError("Description is empty. Aborting...")
2660
2661 if not git_footers.get_footer_change_id(change_desc.description):
2662 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002663 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2664 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002665 ref_to_push = 'HEAD'
2666 parent = '%s/%s' % (gerrit_remote, branch)
2667 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2668
2669 assert change_desc
2670 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2671 ref_to_push)]).splitlines()
2672 if len(commits) > 1:
2673 print('WARNING: This will upload %d commits. Run the following command '
2674 'to see which commits will be uploaded: ' % len(commits))
2675 print('git log %s..%s' % (parent, ref_to_push))
2676 print('You can also use `git squash-branch` to squash these into a '
2677 'single commit.')
2678 ask_for_data('About to upload; enter to confirm.')
2679
2680 if options.reviewers or options.tbr_owners:
2681 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2682 change)
2683
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002684 # Extra options that can be specified at push time. Doc:
2685 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2686 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002687 if change_desc.get_reviewers(tbr_only=True):
2688 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2689 refspec_opts.append('l=Code-Review+1')
2690
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002691 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002692 if not re.match(r'^[\w ]+$', options.title):
2693 options.title = re.sub(r'[^\w ]', '', options.title)
2694 print('WARNING: Patchset title may only contain alphanumeric chars '
2695 'and spaces. Cleaned up title:\n%s' % options.title)
2696 if not options.force:
2697 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002698 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2699 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002700 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2701
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002702 if options.send_mail:
2703 if not change_desc.get_reviewers():
2704 DieWithError('Must specify reviewers to send email.')
2705 refspec_opts.append('notify=ALL')
2706 else:
2707 refspec_opts.append('notify=NONE')
2708
tandrii99a72f22016-08-17 14:33:24 -07002709 reviewers = change_desc.get_reviewers()
2710 if reviewers:
2711 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002712
agablec6787972016-09-09 16:13:34 -07002713 if options.private:
2714 refspec_opts.append('draft')
2715
rmistry9eadede2016-09-19 11:22:43 -07002716 if options.topic:
2717 # Documentation on Gerrit topics is here:
2718 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2719 refspec_opts.append('topic=%s' % options.topic)
2720
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002721 refspec_suffix = ''
2722 if refspec_opts:
2723 refspec_suffix = '%' + ','.join(refspec_opts)
2724 assert ' ' not in refspec_suffix, (
2725 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002726 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002727
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002729 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002730 print_stdout=True,
2731 # Flush after every line: useful for seeing progress when running as
2732 # recipe.
2733 filter_fn=lambda _: sys.stdout.flush())
2734
2735 if options.squash:
2736 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2737 change_numbers = [m.group(1)
2738 for m in map(regex.match, push_stdout.splitlines())
2739 if m]
2740 if len(change_numbers) != 1:
2741 DieWithError(
2742 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2743 'Change-Id: %s') % (len(change_numbers), change_id))
2744 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002745 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002746
2747 # Add cc's from the CC_LIST and --cc flag (if any).
2748 cc = self.GetCCList().split(',')
2749 if options.cc:
2750 cc.extend(options.cc)
2751 cc = filter(None, [email.strip() for email in cc])
2752 if cc:
2753 gerrit_util.AddReviewers(
2754 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2755
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 return 0
2757
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002758 def _AddChangeIdToCommitMessage(self, options, args):
2759 """Re-commits using the current message, assumes the commit hook is in
2760 place.
2761 """
2762 log_desc = options.message or CreateDescriptionFromLog(args)
2763 git_command = ['commit', '--amend', '-m', log_desc]
2764 RunGit(git_command)
2765 new_log_desc = CreateDescriptionFromLog(args)
2766 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002767 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002768 return new_log_desc
2769 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002770 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002771
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002772 def SetCQState(self, new_state):
2773 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002774 vote_map = {
2775 _CQState.NONE: 0,
2776 _CQState.DRY_RUN: 1,
2777 _CQState.COMMIT : 2,
2778 }
2779 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2780 labels={'Commit-Queue': vote_map[new_state]})
2781
tandriie113dfd2016-10-11 10:20:12 -07002782 def CannotTriggerTryJobReason(self):
2783 # TODO(tandrii): implement for Gerrit.
2784 raise NotImplementedError()
2785
tandriide281ae2016-10-12 06:02:30 -07002786 def GetIssueOwner(self):
2787 # TODO(tandrii): implement for Gerrit.
2788 raise NotImplementedError()
2789
2790 def GetIssueProject(self):
2791 # TODO(tandrii): implement for Gerrit.
2792 raise NotImplementedError()
2793
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002794
2795_CODEREVIEW_IMPLEMENTATIONS = {
2796 'rietveld': _RietveldChangelistImpl,
2797 'gerrit': _GerritChangelistImpl,
2798}
2799
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002800
iannuccie53c9352016-08-17 14:40:40 -07002801def _add_codereview_issue_select_options(parser, extra=""):
2802 _add_codereview_select_options(parser)
2803
2804 text = ('Operate on this issue number instead of the current branch\'s '
2805 'implicit issue.')
2806 if extra:
2807 text += ' '+extra
2808 parser.add_option('-i', '--issue', type=int, help=text)
2809
2810
2811def _process_codereview_issue_select_options(parser, options):
2812 _process_codereview_select_options(parser, options)
2813 if options.issue is not None and not options.forced_codereview:
2814 parser.error('--issue must be specified with either --rietveld or --gerrit')
2815
2816
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002817def _add_codereview_select_options(parser):
2818 """Appends --gerrit and --rietveld options to force specific codereview."""
2819 parser.codereview_group = optparse.OptionGroup(
2820 parser, 'EXPERIMENTAL! Codereview override options')
2821 parser.add_option_group(parser.codereview_group)
2822 parser.codereview_group.add_option(
2823 '--gerrit', action='store_true',
2824 help='Force the use of Gerrit for codereview')
2825 parser.codereview_group.add_option(
2826 '--rietveld', action='store_true',
2827 help='Force the use of Rietveld for codereview')
2828
2829
2830def _process_codereview_select_options(parser, options):
2831 if options.gerrit and options.rietveld:
2832 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2833 options.forced_codereview = None
2834 if options.gerrit:
2835 options.forced_codereview = 'gerrit'
2836 elif options.rietveld:
2837 options.forced_codereview = 'rietveld'
2838
2839
tandriif9aefb72016-07-01 09:06:51 -07002840def _get_bug_line_values(default_project, bugs):
2841 """Given default_project and comma separated list of bugs, yields bug line
2842 values.
2843
2844 Each bug can be either:
2845 * a number, which is combined with default_project
2846 * string, which is left as is.
2847
2848 This function may produce more than one line, because bugdroid expects one
2849 project per line.
2850
2851 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2852 ['v8:123', 'chromium:789']
2853 """
2854 default_bugs = []
2855 others = []
2856 for bug in bugs.split(','):
2857 bug = bug.strip()
2858 if bug:
2859 try:
2860 default_bugs.append(int(bug))
2861 except ValueError:
2862 others.append(bug)
2863
2864 if default_bugs:
2865 default_bugs = ','.join(map(str, default_bugs))
2866 if default_project:
2867 yield '%s:%s' % (default_project, default_bugs)
2868 else:
2869 yield default_bugs
2870 for other in sorted(others):
2871 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2872 yield other
2873
2874
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002875class ChangeDescription(object):
2876 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002877 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002878 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002879
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002880 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002881 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002882
agable@chromium.org42c20792013-09-12 17:34:49 +00002883 @property # www.logilab.org/ticket/89786
2884 def description(self): # pylint: disable=E0202
2885 return '\n'.join(self._description_lines)
2886
2887 def set_description(self, desc):
2888 if isinstance(desc, basestring):
2889 lines = desc.splitlines()
2890 else:
2891 lines = [line.rstrip() for line in desc]
2892 while lines and not lines[0]:
2893 lines.pop(0)
2894 while lines and not lines[-1]:
2895 lines.pop(-1)
2896 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002897
piman@chromium.org336f9122014-09-04 02:16:55 +00002898 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002899 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002900 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002901 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002902 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002903 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002904
agable@chromium.org42c20792013-09-12 17:34:49 +00002905 # Get the set of R= and TBR= lines and remove them from the desciption.
2906 regexp = re.compile(self.R_LINE)
2907 matches = [regexp.match(line) for line in self._description_lines]
2908 new_desc = [l for i, l in enumerate(self._description_lines)
2909 if not matches[i]]
2910 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002911
agable@chromium.org42c20792013-09-12 17:34:49 +00002912 # Construct new unified R= and TBR= lines.
2913 r_names = []
2914 tbr_names = []
2915 for match in matches:
2916 if not match:
2917 continue
2918 people = cleanup_list([match.group(2).strip()])
2919 if match.group(1) == 'TBR':
2920 tbr_names.extend(people)
2921 else:
2922 r_names.extend(people)
2923 for name in r_names:
2924 if name not in reviewers:
2925 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002926 if add_owners_tbr:
2927 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002928 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002929 all_reviewers = set(tbr_names + reviewers)
2930 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2931 all_reviewers)
2932 tbr_names.extend(owners_db.reviewers_for(missing_files,
2933 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002934 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2935 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2936
2937 # Put the new lines in the description where the old first R= line was.
2938 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2939 if 0 <= line_loc < len(self._description_lines):
2940 if new_tbr_line:
2941 self._description_lines.insert(line_loc, new_tbr_line)
2942 if new_r_line:
2943 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002944 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002945 if new_r_line:
2946 self.append_footer(new_r_line)
2947 if new_tbr_line:
2948 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002949
tandriif9aefb72016-07-01 09:06:51 -07002950 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002951 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002952 self.set_description([
2953 '# Enter a description of the change.',
2954 '# This will be displayed on the codereview site.',
2955 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002956 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002957 '--------------------',
2958 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002959
agable@chromium.org42c20792013-09-12 17:34:49 +00002960 regexp = re.compile(self.BUG_LINE)
2961 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002962 prefix = settings.GetBugPrefix()
2963 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2964 for value in values:
2965 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2966 self.append_footer('BUG=%s' % value)
2967
agable@chromium.org42c20792013-09-12 17:34:49 +00002968 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002969 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002970 if not content:
2971 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002972 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002973
2974 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002975 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2976 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002977 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002978 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002979
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002980 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002981 """Adds a footer line to the description.
2982
2983 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2984 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2985 that Gerrit footers are always at the end.
2986 """
2987 parsed_footer_line = git_footers.parse_footer(line)
2988 if parsed_footer_line:
2989 # Line is a gerrit footer in the form: Footer-Key: any value.
2990 # Thus, must be appended observing Gerrit footer rules.
2991 self.set_description(
2992 git_footers.add_footer(self.description,
2993 key=parsed_footer_line[0],
2994 value=parsed_footer_line[1]))
2995 return
2996
2997 if not self._description_lines:
2998 self._description_lines.append(line)
2999 return
3000
3001 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3002 if gerrit_footers:
3003 # git_footers.split_footers ensures that there is an empty line before
3004 # actual (gerrit) footers, if any. We have to keep it that way.
3005 assert top_lines and top_lines[-1] == ''
3006 top_lines, separator = top_lines[:-1], top_lines[-1:]
3007 else:
3008 separator = [] # No need for separator if there are no gerrit_footers.
3009
3010 prev_line = top_lines[-1] if top_lines else ''
3011 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3012 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3013 top_lines.append('')
3014 top_lines.append(line)
3015 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003016
tandrii99a72f22016-08-17 14:33:24 -07003017 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003018 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003019 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003020 reviewers = [match.group(2).strip()
3021 for match in matches
3022 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003023 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003024
3025
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003026def get_approving_reviewers(props):
3027 """Retrieves the reviewers that approved a CL from the issue properties with
3028 messages.
3029
3030 Note that the list may contain reviewers that are not committer, thus are not
3031 considered by the CQ.
3032 """
3033 return sorted(
3034 set(
3035 message['sender']
3036 for message in props['messages']
3037 if message['approval'] and message['sender'] in props['reviewers']
3038 )
3039 )
3040
3041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042def FindCodereviewSettingsFile(filename='codereview.settings'):
3043 """Finds the given file starting in the cwd and going up.
3044
3045 Only looks up to the top of the repository unless an
3046 'inherit-review-settings-ok' file exists in the root of the repository.
3047 """
3048 inherit_ok_file = 'inherit-review-settings-ok'
3049 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003050 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003051 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3052 root = '/'
3053 while True:
3054 if filename in os.listdir(cwd):
3055 if os.path.isfile(os.path.join(cwd, filename)):
3056 return open(os.path.join(cwd, filename))
3057 if cwd == root:
3058 break
3059 cwd = os.path.dirname(cwd)
3060
3061
3062def LoadCodereviewSettingsFromFile(fileobj):
3063 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003064 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003066 def SetProperty(name, setting, unset_error_ok=False):
3067 fullname = 'rietveld.' + name
3068 if setting in keyvals:
3069 RunGit(['config', fullname, keyvals[setting]])
3070 else:
3071 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3072
tandrii48df5812016-10-17 03:55:37 -07003073 if not keyvals.get('GERRIT_HOST', False):
3074 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003075 # Only server setting is required. Other settings can be absent.
3076 # In that case, we ignore errors raised during option deletion attempt.
3077 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003078 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003079 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3080 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003081 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003082 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003083 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3084 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003085 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003086 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003087 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003088 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003089 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3090 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003091
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003092 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003093 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003094
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003095 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003096 RunGit(['config', 'gerrit.squash-uploads',
3097 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003098
tandrii@chromium.org28253532016-04-14 13:46:56 +00003099 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003100 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003101 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3104 #should be of the form
3105 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3106 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3107 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3108 keyvals['ORIGIN_URL_CONFIG']])
3109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003111def urlretrieve(source, destination):
3112 """urllib is broken for SSL connections via a proxy therefore we
3113 can't use urllib.urlretrieve()."""
3114 with open(destination, 'w') as f:
3115 f.write(urllib2.urlopen(source).read())
3116
3117
ukai@chromium.org712d6102013-11-27 00:52:58 +00003118def hasSheBang(fname):
3119 """Checks fname is a #! script."""
3120 with open(fname) as f:
3121 return f.read(2).startswith('#!')
3122
3123
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003124# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3125def DownloadHooks(*args, **kwargs):
3126 pass
3127
3128
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003129def DownloadGerritHook(force):
3130 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003131
3132 Args:
3133 force: True to update hooks. False to install hooks if not present.
3134 """
3135 if not settings.GetIsGerrit():
3136 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003137 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003138 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3139 if not os.access(dst, os.X_OK):
3140 if os.path.exists(dst):
3141 if not force:
3142 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003143 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003144 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003145 if not hasSheBang(dst):
3146 DieWithError('Not a script: %s\n'
3147 'You need to download from\n%s\n'
3148 'into .git/hooks/commit-msg and '
3149 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003150 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3151 except Exception:
3152 if os.path.exists(dst):
3153 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003154 DieWithError('\nFailed to download hooks.\n'
3155 'You need to download from\n%s\n'
3156 'into .git/hooks/commit-msg and '
3157 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003158
3159
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003160
3161def GetRietveldCodereviewSettingsInteractively():
3162 """Prompt the user for settings."""
3163 server = settings.GetDefaultServerUrl(error_ok=True)
3164 prompt = 'Rietveld server (host[:port])'
3165 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3166 newserver = ask_for_data(prompt + ':')
3167 if not server and not newserver:
3168 newserver = DEFAULT_SERVER
3169 if newserver:
3170 newserver = gclient_utils.UpgradeToHttps(newserver)
3171 if newserver != server:
3172 RunGit(['config', 'rietveld.server', newserver])
3173
3174 def SetProperty(initial, caption, name, is_url):
3175 prompt = caption
3176 if initial:
3177 prompt += ' ("x" to clear) [%s]' % initial
3178 new_val = ask_for_data(prompt + ':')
3179 if new_val == 'x':
3180 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3181 elif new_val:
3182 if is_url:
3183 new_val = gclient_utils.UpgradeToHttps(new_val)
3184 if new_val != initial:
3185 RunGit(['config', 'rietveld.' + name, new_val])
3186
3187 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3188 SetProperty(settings.GetDefaultPrivateFlag(),
3189 'Private flag (rietveld only)', 'private', False)
3190 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3191 'tree-status-url', False)
3192 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3193 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3194 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3195 'run-post-upload-hook', False)
3196
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003197@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003198def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003199 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003200
tandrii5d0a0422016-09-14 06:24:35 -07003201 print('WARNING: git cl config works for Rietveld only')
3202 # TODO(tandrii): remove this once we switch to Gerrit.
3203 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003204 parser.add_option('--activate-update', action='store_true',
3205 help='activate auto-updating [rietveld] section in '
3206 '.git/config')
3207 parser.add_option('--deactivate-update', action='store_true',
3208 help='deactivate auto-updating [rietveld] section in '
3209 '.git/config')
3210 options, args = parser.parse_args(args)
3211
3212 if options.deactivate_update:
3213 RunGit(['config', 'rietveld.autoupdate', 'false'])
3214 return
3215
3216 if options.activate_update:
3217 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3218 return
3219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003221 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003222 return 0
3223
3224 url = args[0]
3225 if not url.endswith('codereview.settings'):
3226 url = os.path.join(url, 'codereview.settings')
3227
3228 # Load code review settings and download hooks (if available).
3229 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3230 return 0
3231
3232
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003233def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003234 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003235 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3236 branch = ShortBranchName(branchref)
3237 _, args = parser.parse_args(args)
3238 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003239 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003240 return RunGit(['config', 'branch.%s.base-url' % branch],
3241 error_ok=False).strip()
3242 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003243 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003244 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3245 error_ok=False).strip()
3246
3247
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003248def color_for_status(status):
3249 """Maps a Changelist status to color, for CMDstatus and other tools."""
3250 return {
3251 'unsent': Fore.RED,
3252 'waiting': Fore.BLUE,
3253 'reply': Fore.YELLOW,
3254 'lgtm': Fore.GREEN,
3255 'commit': Fore.MAGENTA,
3256 'closed': Fore.CYAN,
3257 'error': Fore.WHITE,
3258 }.get(status, Fore.WHITE)
3259
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003260
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003261def get_cl_statuses(changes, fine_grained, max_processes=None):
3262 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003263
3264 If fine_grained is true, this will fetch CL statuses from the server.
3265 Otherwise, simply indicate if there's a matching url for the given branches.
3266
3267 If max_processes is specified, it is used as the maximum number of processes
3268 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3269 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003270
3271 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003272 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003273 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003274 upload.verbosity = 0
3275
3276 if fine_grained:
3277 # Process one branch synchronously to work through authentication, then
3278 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003279 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003280 def fetch(cl):
3281 try:
3282 return (cl, cl.GetStatus())
3283 except:
3284 # See http://crbug.com/629863.
3285 logging.exception('failed to fetch status for %s:', cl)
3286 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003287 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003288
tandriiea9514a2016-08-17 12:32:37 -07003289 changes_to_fetch = changes[1:]
3290 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003291 # Exit early if there was only one branch to fetch.
3292 return
3293
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003294 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003295 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003296 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003297 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003298
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003299 fetched_cls = set()
3300 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003301 while True:
3302 try:
3303 row = it.next(timeout=5)
3304 except multiprocessing.TimeoutError:
3305 break
3306
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003307 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003308 yield row
3309
3310 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003311 for cl in set(changes_to_fetch) - fetched_cls:
3312 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003313
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003314 else:
3315 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003316 for cl in changes:
3317 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003318
rmistry@google.com2dd99862015-06-22 12:22:18 +00003319
3320def upload_branch_deps(cl, args):
3321 """Uploads CLs of local branches that are dependents of the current branch.
3322
3323 If the local branch dependency tree looks like:
3324 test1 -> test2.1 -> test3.1
3325 -> test3.2
3326 -> test2.2 -> test3.3
3327
3328 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3329 run on the dependent branches in this order:
3330 test2.1, test3.1, test3.2, test2.2, test3.3
3331
3332 Note: This function does not rebase your local dependent branches. Use it when
3333 you make a change to the parent branch that will not conflict with its
3334 dependent branches, and you would like their dependencies updated in
3335 Rietveld.
3336 """
3337 if git_common.is_dirty_git_tree('upload-branch-deps'):
3338 return 1
3339
3340 root_branch = cl.GetBranch()
3341 if root_branch is None:
3342 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3343 'Get on a branch!')
3344 if not cl.GetIssue() or not cl.GetPatchset():
3345 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3346 'patchset dependencies without an uploaded CL.')
3347
3348 branches = RunGit(['for-each-ref',
3349 '--format=%(refname:short) %(upstream:short)',
3350 'refs/heads'])
3351 if not branches:
3352 print('No local branches found.')
3353 return 0
3354
3355 # Create a dictionary of all local branches to the branches that are dependent
3356 # on it.
3357 tracked_to_dependents = collections.defaultdict(list)
3358 for b in branches.splitlines():
3359 tokens = b.split()
3360 if len(tokens) == 2:
3361 branch_name, tracked = tokens
3362 tracked_to_dependents[tracked].append(branch_name)
3363
vapiera7fbd5a2016-06-16 09:17:49 -07003364 print()
3365 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003366 dependents = []
3367 def traverse_dependents_preorder(branch, padding=''):
3368 dependents_to_process = tracked_to_dependents.get(branch, [])
3369 padding += ' '
3370 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003371 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003372 dependents.append(dependent)
3373 traverse_dependents_preorder(dependent, padding)
3374 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003375 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003376
3377 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003378 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003379 return 0
3380
vapiera7fbd5a2016-06-16 09:17:49 -07003381 print('This command will checkout all dependent branches and run '
3382 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003383 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3384
andybons@chromium.org962f9462016-02-03 20:00:42 +00003385 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003386 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003387 args.extend(['-t', 'Updated patchset dependency'])
3388
rmistry@google.com2dd99862015-06-22 12:22:18 +00003389 # Record all dependents that failed to upload.
3390 failures = {}
3391 # Go through all dependents, checkout the branch and upload.
3392 try:
3393 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003394 print()
3395 print('--------------------------------------')
3396 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003397 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003398 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003399 try:
3400 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003401 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003402 failures[dependent_branch] = 1
3403 except: # pylint: disable=W0702
3404 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003405 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003406 finally:
3407 # Swap back to the original root branch.
3408 RunGit(['checkout', '-q', root_branch])
3409
vapiera7fbd5a2016-06-16 09:17:49 -07003410 print()
3411 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003412 for dependent_branch in dependents:
3413 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003414 print(' %s : %s' % (dependent_branch, upload_status))
3415 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003416
3417 return 0
3418
3419
kmarshall3bff56b2016-06-06 18:31:47 -07003420def CMDarchive(parser, args):
3421 """Archives and deletes branches associated with closed changelists."""
3422 parser.add_option(
3423 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003424 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003425 parser.add_option(
3426 '-f', '--force', action='store_true',
3427 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003428 parser.add_option(
3429 '-d', '--dry-run', action='store_true',
3430 help='Skip the branch tagging and removal steps.')
3431 parser.add_option(
3432 '-t', '--notags', action='store_true',
3433 help='Do not tag archived branches. '
3434 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003435
3436 auth.add_auth_options(parser)
3437 options, args = parser.parse_args(args)
3438 if args:
3439 parser.error('Unsupported args: %s' % ' '.join(args))
3440 auth_config = auth.extract_auth_config_from_options(options)
3441
3442 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3443 if not branches:
3444 return 0
3445
vapiera7fbd5a2016-06-16 09:17:49 -07003446 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003447 changes = [Changelist(branchref=b, auth_config=auth_config)
3448 for b in branches.splitlines()]
3449 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3450 statuses = get_cl_statuses(changes,
3451 fine_grained=True,
3452 max_processes=options.maxjobs)
3453 proposal = [(cl.GetBranch(),
3454 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3455 for cl, status in statuses
3456 if status == 'closed']
3457 proposal.sort()
3458
3459 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003460 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003461 return 0
3462
3463 current_branch = GetCurrentBranch()
3464
vapiera7fbd5a2016-06-16 09:17:49 -07003465 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003466 if options.notags:
3467 for next_item in proposal:
3468 print(' ' + next_item[0])
3469 else:
3470 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3471 for next_item in proposal:
3472 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003473
kmarshall9249e012016-08-23 12:02:16 -07003474 # Quit now on precondition failure or if instructed by the user, either
3475 # via an interactive prompt or by command line flags.
3476 if options.dry_run:
3477 print('\nNo changes were made (dry run).\n')
3478 return 0
3479 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003480 print('You are currently on a branch \'%s\' which is associated with a '
3481 'closed codereview issue, so archive cannot proceed. Please '
3482 'checkout another branch and run this command again.' %
3483 current_branch)
3484 return 1
kmarshall9249e012016-08-23 12:02:16 -07003485 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003486 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3487 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003488 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003489 return 1
3490
3491 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003492 if not options.notags:
3493 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003494 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003495
vapiera7fbd5a2016-06-16 09:17:49 -07003496 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003497
3498 return 0
3499
3500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003501def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003502 """Show status of changelists.
3503
3504 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003505 - Red not sent for review or broken
3506 - Blue waiting for review
3507 - Yellow waiting for you to reply to review
3508 - Green LGTM'ed
3509 - Magenta in the commit queue
3510 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003511
3512 Also see 'git cl comments'.
3513 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003515 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003516 parser.add_option('-f', '--fast', action='store_true',
3517 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003518 parser.add_option(
3519 '-j', '--maxjobs', action='store', type=int,
3520 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003521
3522 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003523 _add_codereview_issue_select_options(
3524 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003525 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003526 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003527 if args:
3528 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003529 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003530
iannuccie53c9352016-08-17 14:40:40 -07003531 if options.issue is not None and not options.field:
3532 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003533
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003535 cl = Changelist(auth_config=auth_config, issue=options.issue,
3536 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003538 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539 elif options.field == 'id':
3540 issueid = cl.GetIssue()
3541 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003542 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543 elif options.field == 'patch':
3544 patchset = cl.GetPatchset()
3545 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003546 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003547 elif options.field == 'status':
3548 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 elif options.field == 'url':
3550 url = cl.GetIssueURL()
3551 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003552 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003553 return 0
3554
3555 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3556 if not branches:
3557 print('No local branch found.')
3558 return 0
3559
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003560 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003561 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003562 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003563 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003564 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003565 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003566 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003567
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003568 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003569 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3570 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3571 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003572 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003573 c, status = output.next()
3574 branch_statuses[c.GetBranch()] = status
3575 status = branch_statuses.pop(branch)
3576 url = cl.GetIssueURL()
3577 if url and (not status or status == 'error'):
3578 # The issue probably doesn't exist anymore.
3579 url += ' (broken)'
3580
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003581 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003582 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003583 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003584 color = ''
3585 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003586 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003588 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003589 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003590
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003591 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003592 print()
3593 print('Current branch:',)
3594 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003595 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003596 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003597 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003598 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003599 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003600 print('Issue description:')
3601 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003602 return 0
3603
3604
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003605def colorize_CMDstatus_doc():
3606 """To be called once in main() to add colors to git cl status help."""
3607 colors = [i for i in dir(Fore) if i[0].isupper()]
3608
3609 def colorize_line(line):
3610 for color in colors:
3611 if color in line.upper():
3612 # Extract whitespaces first and the leading '-'.
3613 indent = len(line) - len(line.lstrip(' ')) + 1
3614 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3615 return line
3616
3617 lines = CMDstatus.__doc__.splitlines()
3618 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3619
3620
phajdan.jre328cf92016-08-22 04:12:17 -07003621def write_json(path, contents):
3622 with open(path, 'w') as f:
3623 json.dump(contents, f)
3624
3625
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003626@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003627def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003628 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629
3630 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003631 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003632 parser.add_option('-r', '--reverse', action='store_true',
3633 help='Lookup the branch(es) for the specified issues. If '
3634 'no issues are specified, all branches with mapped '
3635 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003636 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003637 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003638 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003639 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640
dnj@chromium.org406c4402015-03-03 17:22:28 +00003641 if options.reverse:
3642 branches = RunGit(['for-each-ref', 'refs/heads',
3643 '--format=%(refname:short)']).splitlines()
3644
3645 # Reverse issue lookup.
3646 issue_branch_map = {}
3647 for branch in branches:
3648 cl = Changelist(branchref=branch)
3649 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3650 if not args:
3651 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003652 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003653 for issue in args:
3654 if not issue:
3655 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003656 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003657 print('Branch for issue number %s: %s' % (
3658 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003659 if options.json:
3660 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003661 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003662 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003663 if len(args) > 0:
3664 try:
3665 issue = int(args[0])
3666 except ValueError:
3667 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003668 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003669 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003670 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003671 if options.json:
3672 write_json(options.json, {
3673 'issue': cl.GetIssue(),
3674 'issue_url': cl.GetIssueURL(),
3675 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003676 return 0
3677
3678
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003679def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003680 """Shows or posts review comments for any changelist."""
3681 parser.add_option('-a', '--add-comment', dest='comment',
3682 help='comment to add to an issue')
3683 parser.add_option('-i', dest='issue',
3684 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003685 parser.add_option('-j', '--json-file',
3686 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003687 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003688 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003689 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003690
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003691 issue = None
3692 if options.issue:
3693 try:
3694 issue = int(options.issue)
3695 except ValueError:
3696 DieWithError('A review issue id is expected to be a number')
3697
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003698 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003699
3700 if options.comment:
3701 cl.AddComment(options.comment)
3702 return 0
3703
3704 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003705 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003706 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003707 summary.append({
3708 'date': message['date'],
3709 'lgtm': False,
3710 'message': message['text'],
3711 'not_lgtm': False,
3712 'sender': message['sender'],
3713 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003714 if message['disapproval']:
3715 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003716 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003717 elif message['approval']:
3718 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003719 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003720 elif message['sender'] == data['owner_email']:
3721 color = Fore.MAGENTA
3722 else:
3723 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003724 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003725 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003726 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003727 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003729 if options.json_file:
3730 with open(options.json_file, 'wb') as f:
3731 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003732 return 0
3733
3734
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003735@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003736def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003737 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003738 parser.add_option('-d', '--display', action='store_true',
3739 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003740 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003741 help='New description to set for this issue (- for stdin, '
3742 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003743 parser.add_option('-f', '--force', action='store_true',
3744 help='Delete any unpublished Gerrit edits for this issue '
3745 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003746
3747 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003748 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003749 options, args = parser.parse_args(args)
3750 _process_codereview_select_options(parser, options)
3751
3752 target_issue = None
3753 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003754 target_issue = ParseIssueNumberArgument(args[0])
3755 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003756 parser.print_help()
3757 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003758
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003759 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003760
martiniss6eda05f2016-06-30 10:18:35 -07003761 kwargs = {
3762 'auth_config': auth_config,
3763 'codereview': options.forced_codereview,
3764 }
3765 if target_issue:
3766 kwargs['issue'] = target_issue.issue
3767 if options.forced_codereview == 'rietveld':
3768 kwargs['rietveld_server'] = target_issue.hostname
3769
3770 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003771
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003772 if not cl.GetIssue():
3773 DieWithError('This branch has no associated changelist.')
3774 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003775
smut@google.com34fb6b12015-07-13 20:03:26 +00003776 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003778 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003779
3780 if options.new_description:
3781 text = options.new_description
3782 if text == '-':
3783 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003784 elif text == '+':
3785 base_branch = cl.GetCommonAncestorWithUpstream()
3786 change = cl.GetChange(base_branch, None, local_description=True)
3787 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003788
3789 description.set_description(text)
3790 else:
3791 description.prompt()
3792
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003793 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003794 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003795 return 0
3796
3797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003798def CreateDescriptionFromLog(args):
3799 """Pulls out the commit log to use as a base for the CL description."""
3800 log_args = []
3801 if len(args) == 1 and not args[0].endswith('.'):
3802 log_args = [args[0] + '..']
3803 elif len(args) == 1 and args[0].endswith('...'):
3804 log_args = [args[0][:-1]]
3805 elif len(args) == 2:
3806 log_args = [args[0] + '..' + args[1]]
3807 else:
3808 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003809 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003810
3811
thestig@chromium.org44202a22014-03-11 19:22:18 +00003812def CMDlint(parser, args):
3813 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003814 parser.add_option('--filter', action='append', metavar='-x,+y',
3815 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003816 auth.add_auth_options(parser)
3817 options, args = parser.parse_args(args)
3818 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003819
3820 # Access to a protected member _XX of a client class
3821 # pylint: disable=W0212
3822 try:
3823 import cpplint
3824 import cpplint_chromium
3825 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003827 return 1
3828
3829 # Change the current working directory before calling lint so that it
3830 # shows the correct base.
3831 previous_cwd = os.getcwd()
3832 os.chdir(settings.GetRoot())
3833 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003834 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003835 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3836 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003837 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003838 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003839 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003840
3841 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003842 command = args + files
3843 if options.filter:
3844 command = ['--filter=' + ','.join(options.filter)] + command
3845 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003846
3847 white_regex = re.compile(settings.GetLintRegex())
3848 black_regex = re.compile(settings.GetLintIgnoreRegex())
3849 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3850 for filename in filenames:
3851 if white_regex.match(filename):
3852 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003854 else:
3855 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3856 extra_check_functions)
3857 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003858 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003859 finally:
3860 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003861 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003862 if cpplint._cpplint_state.error_count != 0:
3863 return 1
3864 return 0
3865
3866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003868 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003869 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003871 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003872 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003873 auth.add_auth_options(parser)
3874 options, args = parser.parse_args(args)
3875 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876
sbc@chromium.org71437c02015-04-09 19:29:40 +00003877 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003878 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003879 return 1
3880
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003881 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882 if args:
3883 base_branch = args[0]
3884 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003885 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003886 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003887
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003888 cl.RunHook(
3889 committing=not options.upload,
3890 may_prompt=False,
3891 verbose=options.verbose,
3892 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003893 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894
3895
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003896def GenerateGerritChangeId(message):
3897 """Returns Ixxxxxx...xxx change id.
3898
3899 Works the same way as
3900 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3901 but can be called on demand on all platforms.
3902
3903 The basic idea is to generate git hash of a state of the tree, original commit
3904 message, author/committer info and timestamps.
3905 """
3906 lines = []
3907 tree_hash = RunGitSilent(['write-tree'])
3908 lines.append('tree %s' % tree_hash.strip())
3909 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3910 if code == 0:
3911 lines.append('parent %s' % parent.strip())
3912 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3913 lines.append('author %s' % author.strip())
3914 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3915 lines.append('committer %s' % committer.strip())
3916 lines.append('')
3917 # Note: Gerrit's commit-hook actually cleans message of some lines and
3918 # whitespace. This code is not doing this, but it clearly won't decrease
3919 # entropy.
3920 lines.append(message)
3921 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3922 stdin='\n'.join(lines))
3923 return 'I%s' % change_hash.strip()
3924
3925
wittman@chromium.org455dc922015-01-26 20:15:50 +00003926def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3927 """Computes the remote branch ref to use for the CL.
3928
3929 Args:
3930 remote (str): The git remote for the CL.
3931 remote_branch (str): The git remote branch for the CL.
3932 target_branch (str): The target branch specified by the user.
3933 pending_prefix (str): The pending prefix from the settings.
3934 """
3935 if not (remote and remote_branch):
3936 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003937
wittman@chromium.org455dc922015-01-26 20:15:50 +00003938 if target_branch:
3939 # Cannonicalize branch references to the equivalent local full symbolic
3940 # refs, which are then translated into the remote full symbolic refs
3941 # below.
3942 if '/' not in target_branch:
3943 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3944 else:
3945 prefix_replacements = (
3946 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3947 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3948 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3949 )
3950 match = None
3951 for regex, replacement in prefix_replacements:
3952 match = re.search(regex, target_branch)
3953 if match:
3954 remote_branch = target_branch.replace(match.group(0), replacement)
3955 break
3956 if not match:
3957 # This is a branch path but not one we recognize; use as-is.
3958 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003959 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3960 # Handle the refs that need to land in different refs.
3961 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003962
wittman@chromium.org455dc922015-01-26 20:15:50 +00003963 # Create the true path to the remote branch.
3964 # Does the following translation:
3965 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3966 # * refs/remotes/origin/master -> refs/heads/master
3967 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3968 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3969 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3970 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3971 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3972 'refs/heads/')
3973 elif remote_branch.startswith('refs/remotes/branch-heads'):
3974 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3975 # If a pending prefix exists then replace refs/ with it.
3976 if pending_prefix:
3977 remote_branch = remote_branch.replace('refs/', pending_prefix)
3978 return remote_branch
3979
3980
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003981def cleanup_list(l):
3982 """Fixes a list so that comma separated items are put as individual items.
3983
3984 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3985 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3986 """
3987 items = sum((i.split(',') for i in l), [])
3988 stripped_items = (i.strip() for i in items)
3989 return sorted(filter(None, stripped_items))
3990
3991
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003992@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003993def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003994 """Uploads the current changelist to codereview.
3995
3996 Can skip dependency patchset uploads for a branch by running:
3997 git config branch.branch_name.skip-deps-uploads True
3998 To unset run:
3999 git config --unset branch.branch_name.skip-deps-uploads
4000 Can also set the above globally by using the --global flag.
4001 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004002 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4003 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004004 parser.add_option('--bypass-watchlists', action='store_true',
4005 dest='bypass_watchlists',
4006 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004007 parser.add_option('-f', action='store_true', dest='force',
4008 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004009 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004010 parser.add_option('-b', '--bug',
4011 help='pre-populate the bug number(s) for this issue. '
4012 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004013 parser.add_option('--message-file', dest='message_file',
4014 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004015 parser.add_option('-t', dest='title',
4016 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004017 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004018 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004019 help='reviewer email addresses')
4020 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004021 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004022 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004023 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004024 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004025 parser.add_option('--emulate_svn_auto_props',
4026 '--emulate-svn-auto-props',
4027 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004028 dest="emulate_svn_auto_props",
4029 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004030 parser.add_option('-c', '--use-commit-queue', action='store_true',
4031 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004032 parser.add_option('--private', action='store_true',
4033 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004034 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004035 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004036 metavar='TARGET',
4037 help='Apply CL to remote ref TARGET. ' +
4038 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004039 parser.add_option('--squash', action='store_true',
4040 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004041 parser.add_option('--no-squash', action='store_true',
4042 help='Don\'t squash multiple commits into one ' +
4043 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004044 parser.add_option('--topic', default=None,
4045 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004046 parser.add_option('--email', default=None,
4047 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004048 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4049 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004050 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4051 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004052 help='Send the patchset to do a CQ dry run right after '
4053 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004054 parser.add_option('--dependencies', action='store_true',
4055 help='Uploads CLs of all the local branches that depend on '
4056 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004057
rmistry@google.com2dd99862015-06-22 12:22:18 +00004058 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004059 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004060 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004061 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004062 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004063 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004064 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004065
sbc@chromium.org71437c02015-04-09 19:29:40 +00004066 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004067 return 1
4068
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004069 options.reviewers = cleanup_list(options.reviewers)
4070 options.cc = cleanup_list(options.cc)
4071
tandriib80458a2016-06-23 12:20:07 -07004072 if options.message_file:
4073 if options.message:
4074 parser.error('only one of --message and --message-file allowed.')
4075 options.message = gclient_utils.FileRead(options.message_file)
4076 options.message_file = None
4077
tandrii4d0545a2016-07-06 03:56:49 -07004078 if options.cq_dry_run and options.use_commit_queue:
4079 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4080
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004081 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4082 settings.GetIsGerrit()
4083
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004084 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004085 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004086
4087
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004088def IsSubmoduleMergeCommit(ref):
4089 # When submodules are added to the repo, we expect there to be a single
4090 # non-git-svn merge commit at remote HEAD with a signature comment.
4091 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004092 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004093 return RunGit(cmd) != ''
4094
4095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004096def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004097 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004099 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4100 upstream and closes the issue automatically and atomically.
4101
4102 Otherwise (in case of Rietveld):
4103 Squashes branch into a single commit.
4104 Updates changelog with metadata (e.g. pointer to review).
4105 Pushes/dcommits the code upstream.
4106 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004107 """
4108 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4109 help='bypass upload presubmit hook')
4110 parser.add_option('-m', dest='message',
4111 help="override review description")
4112 parser.add_option('-f', action='store_true', dest='force',
4113 help="force yes to questions (don't prompt)")
4114 parser.add_option('-c', dest='contributor',
4115 help="external contributor for patch (appended to " +
4116 "description and used as author for git). Should be " +
4117 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004118 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004119 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004120 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004121 auth_config = auth.extract_auth_config_from_options(options)
4122
4123 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004124
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004125 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4126 if cl.IsGerrit():
4127 if options.message:
4128 # This could be implemented, but it requires sending a new patch to
4129 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4130 # Besides, Gerrit has the ability to change the commit message on submit
4131 # automatically, thus there is no need to support this option (so far?).
4132 parser.error('-m MESSAGE option is not supported for Gerrit.')
4133 if options.contributor:
4134 parser.error(
4135 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4136 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4137 'the contributor\'s "name <email>". If you can\'t upload such a '
4138 'commit for review, contact your repository admin and request'
4139 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004140 if not cl.GetIssue():
4141 DieWithError('You must upload the issue first to Gerrit.\n'
4142 ' If you would rather have `git cl land` upload '
4143 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004144 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4145 options.verbose)
4146
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004147 current = cl.GetBranch()
4148 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4149 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004150 print()
4151 print('Attempting to push branch %r into another local branch!' % current)
4152 print()
4153 print('Either reparent this branch on top of origin/master:')
4154 print(' git reparent-branch --root')
4155 print()
4156 print('OR run `git rebase-update` if you think the parent branch is ')
4157 print('already committed.')
4158 print()
4159 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004160 return 1
4161
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004162 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004163 # Default to merging against our best guess of the upstream branch.
4164 args = [cl.GetUpstreamBranch()]
4165
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004166 if options.contributor:
4167 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004169 return 1
4170
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004172 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173
sbc@chromium.org71437c02015-04-09 19:29:40 +00004174 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004175 return 1
4176
4177 # This rev-list syntax means "show all commits not in my branch that
4178 # are in base_branch".
4179 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4180 base_branch]).splitlines()
4181 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004182 print('Base branch "%s" has %d commits '
4183 'not in this branch.' % (base_branch, len(upstream_commits)))
4184 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185 return 1
4186
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004187 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004188 svn_head = None
4189 if cmd == 'dcommit' or base_has_submodules:
4190 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4191 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004194 # If the base_head is a submodule merge commit, the first parent of the
4195 # base_head should be a git-svn commit, which is what we're interested in.
4196 base_svn_head = base_branch
4197 if base_has_submodules:
4198 base_svn_head += '^1'
4199
4200 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004201 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004202 print('This branch has %d additional commits not upstreamed yet.'
4203 % len(extra_commits.splitlines()))
4204 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4205 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004206 return 1
4207
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004208 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004209 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004210 author = None
4211 if options.contributor:
4212 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004213 hook_results = cl.RunHook(
4214 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004215 may_prompt=not options.force,
4216 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004217 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004218 if not hook_results.should_continue():
4219 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004221 # Check the tree status if the tree status URL is set.
4222 status = GetTreeStatus()
4223 if 'closed' == status:
4224 print('The tree is closed. Please wait for it to reopen. Use '
4225 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4226 return 1
4227 elif 'unknown' == status:
4228 print('Unable to determine tree status. Please verify manually and '
4229 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4230 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004232 change_desc = ChangeDescription(options.message)
4233 if not change_desc.description and cl.GetIssue():
4234 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004236 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004237 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004238 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004239 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004240 print('No description set.')
4241 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004242 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004244 # Keep a separate copy for the commit message, because the commit message
4245 # contains the link to the Rietveld issue, while the Rietveld message contains
4246 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004247 # Keep a separate copy for the commit message.
4248 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004249 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004250
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004251 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004252 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004253 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004254 # after it. Add a period on a new line to circumvent this. Also add a space
4255 # before the period to make sure that Gitiles continues to correctly resolve
4256 # the URL.
4257 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004259 commit_desc.append_footer('Patch from %s.' % options.contributor)
4260
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004261 print('Description:')
4262 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004263
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004264 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004266 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004268 # We want to squash all this branch's commits into one commit with the proper
4269 # description. We do this by doing a "reset --soft" to the base branch (which
4270 # keeps the working copy the same), then dcommitting that. If origin/master
4271 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4272 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004274 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4275 # Delete the branches if they exist.
4276 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4277 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4278 result = RunGitWithCode(showref_cmd)
4279 if result[0] == 0:
4280 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281
4282 # We might be in a directory that's present in this branch but not in the
4283 # trunk. Move up to the top of the tree so that git commands that expect a
4284 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004285 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004286 if rel_base_path:
4287 os.chdir(rel_base_path)
4288
4289 # Stuff our change into the merge branch.
4290 # We wrap in a try...finally block so if anything goes wrong,
4291 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004292 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004293 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004294 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004295 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004297 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004298 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004300 RunGit(
4301 [
4302 'commit', '--author', options.contributor,
4303 '-m', commit_desc.description,
4304 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004306 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004307 if base_has_submodules:
4308 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4309 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4310 RunGit(['checkout', CHERRY_PICK_BRANCH])
4311 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004312 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004313 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004314 mirror = settings.GetGitMirror(remote)
4315 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004316 pending_prefix = settings.GetPendingRefPrefix()
4317 if not pending_prefix or branch.startswith(pending_prefix):
4318 # If not using refs/pending/heads/* at all, or target ref is already set
4319 # to pending, then push to the target ref directly.
4320 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004321 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004322 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004323 else:
4324 # Cherry-pick the change on top of pending ref and then push it.
4325 assert branch.startswith('refs/'), branch
4326 assert pending_prefix[-1] == '/', pending_prefix
4327 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004328 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004329 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004330 if retcode == 0:
4331 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 else:
4333 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004334 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004335 'svn', 'dcommit',
4336 '-C%s' % options.similarity,
4337 '--no-rebase', '--rmdir',
4338 ]
4339 if settings.GetForceHttpsCommitUrl():
4340 # Allow forcing https commit URLs for some projects that don't allow
4341 # committing to http URLs (like Google Code).
4342 remote_url = cl.GetGitSvnRemoteUrl()
4343 if urlparse.urlparse(remote_url).scheme == 'http':
4344 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004345 cmd_args.append('--commit-url=%s' % remote_url)
4346 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004347 if 'Committed r' in output:
4348 revision = re.match(
4349 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4350 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351 finally:
4352 # And then swap back to the original branch and clean up.
4353 RunGit(['checkout', '-q', cl.GetBranch()])
4354 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004355 if base_has_submodules:
4356 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004358 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004359 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004360 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004361
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004362 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004363 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004364 try:
4365 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4366 # We set pushed_to_pending to False, since it made it all the way to the
4367 # real ref.
4368 pushed_to_pending = False
4369 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004370 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004371
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004372 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004373 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004375 if not to_pending:
4376 if viewvc_url and revision:
4377 change_desc.append_footer(
4378 'Committed: %s%s' % (viewvc_url, revision))
4379 elif revision:
4380 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('Closing issue '
4382 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004383 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004385 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004386 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004387 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004388 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004389 if options.bypass_hooks:
4390 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4391 else:
4392 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004393 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004394
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004395 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004396 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004397 print('The commit is in the pending queue (%s).' % pending_ref)
4398 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4399 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004400
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004401 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4402 if os.path.isfile(hook):
4403 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004404
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004405 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406
4407
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004408def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print()
4410 print('Waiting for commit to be landed on %s...' % real_ref)
4411 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004412 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4413 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004414 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004415
4416 loop = 0
4417 while True:
4418 sys.stdout.write('fetching (%d)... \r' % loop)
4419 sys.stdout.flush()
4420 loop += 1
4421
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004422 if mirror:
4423 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004424 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4425 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4426 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4427 for commit in commits.splitlines():
4428 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004429 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004430 return commit
4431
4432 current_rev = to_rev
4433
4434
tandriibf429402016-09-14 07:09:12 -07004435def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004436 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4437
4438 Returns:
4439 (retcode of last operation, output log of last operation).
4440 """
4441 assert pending_ref.startswith('refs/'), pending_ref
4442 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4443 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4444 code = 0
4445 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004446 max_attempts = 3
4447 attempts_left = max_attempts
4448 while attempts_left:
4449 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004451 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004452
4453 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004454 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004455 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004456 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004457 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004458 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004459 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004460 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004461 continue
4462
4463 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004464 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004465 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004466 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004467 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004468 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4469 'the following files have merge conflicts:' % pending_ref)
4470 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4471 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004472 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004473 return code, out
4474
4475 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004476 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004477 code, out = RunGitWithCode(
4478 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4479 if code == 0:
4480 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004481 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004482 return code, out
4483
vapiera7fbd5a2016-06-16 09:17:49 -07004484 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004485 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004486 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004487 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004488 print('Fatal push error. Make sure your .netrc credentials and git '
4489 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004490 return code, out
4491
vapiera7fbd5a2016-06-16 09:17:49 -07004492 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004493 return code, out
4494
4495
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004496def IsFatalPushFailure(push_stdout):
4497 """True if retrying push won't help."""
4498 return '(prohibited by Gerrit)' in push_stdout
4499
4500
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004501@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004502def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004503 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004505 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004506 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004507 message = """This repository appears to be a git-svn mirror, but we
4508don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004509 else:
4510 message = """This doesn't appear to be an SVN repository.
4511If your project has a true, writeable git repository, you probably want to run
4512'git cl land' instead.
4513If your project has a git mirror of an upstream SVN master, you probably need
4514to run 'git svn init'.
4515
4516Using the wrong command might cause your commit to appear to succeed, and the
4517review to be closed, without actually landing upstream. If you choose to
4518proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004519 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004520 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004521 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4522 'Please let us know of this project you are committing to:'
4523 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004524 return SendUpstream(parser, args, 'dcommit')
4525
4526
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004527@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004528def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004529 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004530 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531 print('This appears to be an SVN repository.')
4532 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004533 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004534 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004535 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536
4537
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004538@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004540 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 parser.add_option('-b', dest='newbranch',
4542 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004543 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004544 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004545 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4546 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004547 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004548 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004549 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004550 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004551 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004552 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004553
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004554
4555 group = optparse.OptionGroup(
4556 parser,
4557 'Options for continuing work on the current issue uploaded from a '
4558 'different clone (e.g. different machine). Must be used independently '
4559 'from the other options. No issue number should be specified, and the '
4560 'branch must have an issue number associated with it')
4561 group.add_option('--reapply', action='store_true', dest='reapply',
4562 help='Reset the branch and reapply the issue.\n'
4563 'CAUTION: This will undo any local changes in this '
4564 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004565
4566 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004567 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004568 parser.add_option_group(group)
4569
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004570 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004571 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004572 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004573 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004574 auth_config = auth.extract_auth_config_from_options(options)
4575
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004576
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004577 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004578 if options.newbranch:
4579 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004580 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004581 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004582
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004583 cl = Changelist(auth_config=auth_config,
4584 codereview=options.forced_codereview)
4585 if not cl.GetIssue():
4586 parser.error('current branch must have an associated issue')
4587
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004588 upstream = cl.GetUpstreamBranch()
4589 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004590 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004591
4592 RunGit(['reset', '--hard', upstream])
4593 if options.pull:
4594 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004595
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004596 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4597 options.directory)
4598
4599 if len(args) != 1 or not args[0]:
4600 parser.error('Must specify issue number or url')
4601
4602 # We don't want uncommitted changes mixed up with the patch.
4603 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004604 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004606 if options.newbranch:
4607 if options.force:
4608 RunGit(['branch', '-D', options.newbranch],
4609 stderr=subprocess2.PIPE, error_ok=True)
4610 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004611 elif not GetCurrentBranch():
4612 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004613
4614 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4615
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004616 if cl.IsGerrit():
4617 if options.reject:
4618 parser.error('--reject is not supported with Gerrit codereview.')
4619 if options.nocommit:
4620 parser.error('--nocommit is not supported with Gerrit codereview.')
4621 if options.directory:
4622 parser.error('--directory is not supported with Gerrit codereview.')
4623
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004624 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004625 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626
4627
4628def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004629 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004630 # Provide a wrapper for git svn rebase to help avoid accidental
4631 # git svn dcommit.
4632 # It's the only command that doesn't use parser at all since we just defer
4633 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004634
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004635 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004636
4637
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004638def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639 """Fetches the tree status and returns either 'open', 'closed',
4640 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004641 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642 if url:
4643 status = urllib2.urlopen(url).read().lower()
4644 if status.find('closed') != -1 or status == '0':
4645 return 'closed'
4646 elif status.find('open') != -1 or status == '1':
4647 return 'open'
4648 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649 return 'unset'
4650
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004652def GetTreeStatusReason():
4653 """Fetches the tree status from a json url and returns the message
4654 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004655 url = settings.GetTreeStatusUrl()
4656 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004657 connection = urllib2.urlopen(json_url)
4658 status = json.loads(connection.read())
4659 connection.close()
4660 return status['message']
4661
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004662
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004663def GetBuilderMaster(bot_list):
4664 """For a given builder, fetch the master from AE if available."""
4665 map_url = 'https://builders-map.appspot.com/'
4666 try:
4667 master_map = json.load(urllib2.urlopen(map_url))
4668 except urllib2.URLError as e:
4669 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4670 (map_url, e))
4671 except ValueError as e:
4672 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4673 if not master_map:
4674 return None, 'Failed to build master map.'
4675
4676 result_master = ''
4677 for bot in bot_list:
4678 builder = bot.split(':', 1)[0]
4679 master_list = master_map.get(builder, [])
4680 if not master_list:
4681 return None, ('No matching master for builder %s.' % builder)
4682 elif len(master_list) > 1:
4683 return None, ('The builder name %s exists in multiple masters %s.' %
4684 (builder, master_list))
4685 else:
4686 cur_master = master_list[0]
4687 if not result_master:
4688 result_master = cur_master
4689 elif result_master != cur_master:
4690 return None, 'The builders do not belong to the same master.'
4691 return result_master, None
4692
4693
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004695 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004696 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004697 status = GetTreeStatus()
4698 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004699 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004700 return 2
4701
vapiera7fbd5a2016-06-16 09:17:49 -07004702 print('The tree is %s' % status)
4703 print()
4704 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004705 if status != 'open':
4706 return 1
4707 return 0
4708
4709
maruel@chromium.org15192402012-09-06 12:38:29 +00004710def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004711 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4712 """
tandrii1838bad2016-10-06 00:10:52 -07004713 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004714 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004715 '-b', '--bot', action='append',
4716 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4717 'times to specify multiple builders. ex: '
4718 '"-b win_rel -b win_layout". See '
4719 'the try server waterfall for the builders name and the tests '
4720 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004721 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004722 '-m', '--master', default='',
4723 help=('Specify a try master where to run the tries.'))
tandriif7b29d42016-10-07 08:45:41 -07004724 # TODO(tandrii,nodir): add -B --bucket flag.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004725 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004726 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004727 help='Revision to use for the try job; default: the revision will '
4728 'be determined by the try recipe that builder runs, which usually '
4729 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004730 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004731 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004732 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004733 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004734 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004735 '--project',
4736 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004737 'in recipe to determine to which repository or directory to '
4738 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004739 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004740 '-p', '--property', dest='properties', action='append', default=[],
4741 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004742 'key2=value2 etc. The value will be treated as '
4743 'json if decodable, or as string otherwise. '
4744 'NOTE: using this may make your try job not usable for CQ, '
4745 'which will then schedule another try job with default properties')
4746 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004747 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004748 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004749 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004750 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4751 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004752 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004753 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004754 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004755 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004756
machenbach@chromium.org45453142015-09-15 08:45:22 +00004757 # Make sure that all properties are prop=value pairs.
4758 bad_params = [x for x in options.properties if '=' not in x]
4759 if bad_params:
4760 parser.error('Got properties with missing "=": %s' % bad_params)
4761
maruel@chromium.org15192402012-09-06 12:38:29 +00004762 if args:
4763 parser.error('Unknown arguments: %s' % args)
4764
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004765 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004766 if not cl.GetIssue():
4767 parser.error('Need to upload first')
4768
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004769 if cl.IsGerrit():
4770 parser.error(
4771 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4772 'If your project has Commit Queue, dry run is a workaround:\n'
4773 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004774
tandriie113dfd2016-10-11 10:20:12 -07004775 error_message = cl.CannotTriggerTryJobReason()
4776 if error_message:
4777 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004778
maruel@chromium.org15192402012-09-06 12:38:29 +00004779 if not options.name:
4780 options.name = cl.GetBranch()
4781
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004782 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004783 options.master, err_msg = GetBuilderMaster(options.bot)
4784 if err_msg:
4785 parser.error('Tryserver master cannot be found because: %s\n'
4786 'Please manually specify the tryserver master'
4787 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004788
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004789 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004790 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004791 if not options.bot:
4792 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004793
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004794 # Get try masters from PRESUBMIT.py files.
4795 masters = presubmit_support.DoGetTryMasters(
4796 change,
4797 change.LocalPaths(),
4798 settings.GetRoot(),
4799 None,
4800 None,
4801 options.verbose,
4802 sys.stdout)
4803 if masters:
4804 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004805
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004806 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4807 options.bot = presubmit_support.DoGetTrySlaves(
4808 change,
4809 change.LocalPaths(),
4810 settings.GetRoot(),
4811 None,
4812 None,
4813 options.verbose,
4814 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004815
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004816 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004817 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004818
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004819 builders_and_tests = {}
4820 # TODO(machenbach): The old style command-line options don't support
4821 # multiple try masters yet.
4822 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4823 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4824
4825 for bot in old_style:
4826 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004827 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004828 elif ',' in bot:
4829 parser.error('Specify one bot per --bot flag')
4830 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004831 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004832
4833 for bot, tests in new_style:
4834 builders_and_tests.setdefault(bot, []).extend(tests)
4835
4836 # Return a master map with one master to be backwards compatible. The
4837 # master name defaults to an empty string, which will cause the master
4838 # not to be set on rietveld (deprecated).
4839 return {options.master: builders_and_tests}
4840
4841 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004842 if not masters:
4843 # Default to triggering Dry Run (see http://crbug.com/625697).
4844 if options.verbose:
4845 print('git cl try with no bots now defaults to CQ Dry Run.')
4846 try:
4847 cl.SetCQState(_CQState.DRY_RUN)
4848 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4849 return 0
4850 except KeyboardInterrupt:
4851 raise
4852 except:
4853 print('WARNING: failed to trigger CQ Dry Run.\n'
4854 'Either:\n'
4855 ' * your project has no CQ\n'
4856 ' * you don\'t have permission to trigger Dry Run\n'
4857 ' * bug in this code (see stack trace below).\n'
4858 'Consider specifying which bots to trigger manually '
4859 'or asking your project owners for permissions '
4860 'or contacting Chrome Infrastructure team at '
4861 'https://www.chromium.org/infra\n\n')
4862 # Still raise exception so that stack trace is printed.
4863 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004864
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004865 for builders in masters.itervalues():
4866 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004867 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004868 'of bot requires an initial job from a parent (usually a builder). '
4869 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004870 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004871 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004872
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004873 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004874 if patchset != cl.GetPatchset():
4875 print('Warning: Codereview server has newer patchsets (%s) than most '
4876 'recent upload from local checkout (%s). Did a previous upload '
4877 'fail?\n'
4878 'By default, git cl try uses the latest patchset from '
4879 'codereview, continuing to use patchset %s.\n' %
4880 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004881 try:
tandriide281ae2016-10-12 06:02:30 -07004882 _trigger_try_jobs(auth_config, cl, masters, options, 'git_cl_try',
4883 patchset)
tandrii568043b2016-10-11 07:49:18 -07004884 except BuildbucketResponseException as ex:
4885 print('ERROR: %s' % ex)
4886 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004887 return 0
4888
4889
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004890def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004891 """Prints info about try jobs associated with current CL."""
4892 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004893 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004894 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004895 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004896 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004897 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004898 '--color', action='store_true', default=setup_color.IS_TTY,
4899 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004900 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004901 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4902 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004903 group.add_option(
4904 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004905 parser.add_option_group(group)
4906 auth.add_auth_options(parser)
4907 options, args = parser.parse_args(args)
4908 if args:
4909 parser.error('Unrecognized args: %s' % ' '.join(args))
4910
4911 auth_config = auth.extract_auth_config_from_options(options)
4912 cl = Changelist(auth_config=auth_config)
4913 if not cl.GetIssue():
4914 parser.error('Need to upload first')
4915
tandrii221ab252016-10-06 08:12:04 -07004916 patchset = options.patchset
4917 if not patchset:
4918 patchset = cl.GetMostRecentPatchset()
4919 if not patchset:
4920 parser.error('Codereview doesn\'t know about issue %s. '
4921 'No access to issue or wrong issue number?\n'
4922 'Either upload first, or pass --patchset explicitely' %
4923 cl.GetIssue())
4924
4925 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004926 print('Warning: Codereview server has newer patchsets (%s) than most '
4927 'recent upload from local checkout (%s). Did a previous upload '
4928 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004929 'By default, git cl try-results uses the latest patchset from '
4930 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004931 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004932 try:
tandrii221ab252016-10-06 08:12:04 -07004933 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004934 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004935 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004936 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004937 if options.json:
4938 write_try_results_json(options.json, jobs)
4939 else:
4940 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004941 return 0
4942
4943
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004944@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004945def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004946 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004947 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004948 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004949 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004951 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004952 if args:
4953 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004954 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004955 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004956 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004957 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004958
4959 # Clear configured merge-base, if there is one.
4960 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004961 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004962 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004963 return 0
4964
4965
thestig@chromium.org00858c82013-12-02 23:08:03 +00004966def CMDweb(parser, args):
4967 """Opens the current CL in the web browser."""
4968 _, args = parser.parse_args(args)
4969 if args:
4970 parser.error('Unrecognized args: %s' % ' '.join(args))
4971
4972 issue_url = Changelist().GetIssueURL()
4973 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004974 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004975 return 1
4976
4977 webbrowser.open(issue_url)
4978 return 0
4979
4980
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004981def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004982 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004983 parser.add_option('-d', '--dry-run', action='store_true',
4984 help='trigger in dry run mode')
4985 parser.add_option('-c', '--clear', action='store_true',
4986 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004987 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004988 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004989 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004990 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004991 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004992 if args:
4993 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004994 if options.dry_run and options.clear:
4995 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4996
iannuccie53c9352016-08-17 14:40:40 -07004997 cl = Changelist(auth_config=auth_config, issue=options.issue,
4998 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004999 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005000 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005001 elif options.dry_run:
5002 state = _CQState.DRY_RUN
5003 else:
5004 state = _CQState.COMMIT
5005 if not cl.GetIssue():
5006 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005007 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005008 return 0
5009
5010
groby@chromium.org411034a2013-02-26 15:12:01 +00005011def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005012 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005013 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005014 auth.add_auth_options(parser)
5015 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005016 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005017 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005018 if args:
5019 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005020 cl = Changelist(auth_config=auth_config, issue=options.issue,
5021 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005022 # Ensure there actually is an issue to close.
5023 cl.GetDescription()
5024 cl.CloseIssue()
5025 return 0
5026
5027
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005028def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005029 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005030 parser.add_option(
5031 '--stat',
5032 action='store_true',
5033 dest='stat',
5034 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005035 auth.add_auth_options(parser)
5036 options, args = parser.parse_args(args)
5037 auth_config = auth.extract_auth_config_from_options(options)
5038 if args:
5039 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005040
5041 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005042 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005043 # Staged changes would be committed along with the patch from last
5044 # upload, hence counted toward the "last upload" side in the final
5045 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005046 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005047 return 1
5048
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005049 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005050 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005051 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005052 if not issue:
5053 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005054 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005055 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005056
5057 # Create a new branch based on the merge-base
5058 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005059 # Clear cached branch in cl object, to avoid overwriting original CL branch
5060 # properties.
5061 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005062 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005063 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005064 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005065 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005066 return rtn
5067
wychen@chromium.org06928532015-02-03 02:11:29 +00005068 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005069 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005070 cmd = ['git', 'diff']
5071 if options.stat:
5072 cmd.append('--stat')
5073 cmd.extend([TMP_BRANCH, branch, '--'])
5074 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005075 finally:
5076 RunGit(['checkout', '-q', branch])
5077 RunGit(['branch', '-D', TMP_BRANCH])
5078
5079 return 0
5080
5081
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005082def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005083 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005084 parser.add_option(
5085 '--no-color',
5086 action='store_true',
5087 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005088 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005089 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005090 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005091
5092 author = RunGit(['config', 'user.email']).strip() or None
5093
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005094 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005095
5096 if args:
5097 if len(args) > 1:
5098 parser.error('Unknown args')
5099 base_branch = args[0]
5100 else:
5101 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005102 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005103
5104 change = cl.GetChange(base_branch, None)
5105 return owners_finder.OwnersFinder(
5106 [f.LocalPath() for f in
5107 cl.GetChange(base_branch, None).AffectedFiles()],
5108 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005109 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005110 disable_color=options.no_color).run()
5111
5112
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005113def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005114 """Generates a diff command."""
5115 # Generate diff for the current branch's changes.
5116 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5117 upstream_commit, '--' ]
5118
5119 if args:
5120 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005121 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005122 diff_cmd.append(arg)
5123 else:
5124 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005125
5126 return diff_cmd
5127
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128def MatchingFileType(file_name, extensions):
5129 """Returns true if the file name ends with one of the given extensions."""
5130 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005131
enne@chromium.org555cfe42014-01-29 18:21:39 +00005132@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005133def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005134 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005135 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005136 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005137 parser.add_option('--full', action='store_true',
5138 help='Reformat the full content of all touched files')
5139 parser.add_option('--dry-run', action='store_true',
5140 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005141 parser.add_option('--python', action='store_true',
5142 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005143 parser.add_option('--diff', action='store_true',
5144 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005145 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005146
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005147 # git diff generates paths against the root of the repository. Change
5148 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005149 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005150 if rel_base_path:
5151 os.chdir(rel_base_path)
5152
digit@chromium.org29e47272013-05-17 17:01:46 +00005153 # Grab the merge-base commit, i.e. the upstream commit of the current
5154 # branch when it was created or the last time it was rebased. This is
5155 # to cover the case where the user may have called "git fetch origin",
5156 # moving the origin branch to a newer commit, but hasn't rebased yet.
5157 upstream_commit = None
5158 cl = Changelist()
5159 upstream_branch = cl.GetUpstreamBranch()
5160 if upstream_branch:
5161 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5162 upstream_commit = upstream_commit.strip()
5163
5164 if not upstream_commit:
5165 DieWithError('Could not find base commit for this branch. '
5166 'Are you in detached state?')
5167
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005168 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5169 diff_output = RunGit(changed_files_cmd)
5170 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005171 # Filter out files deleted by this CL
5172 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005173
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005174 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5175 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5176 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005177 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005178
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005179 top_dir = os.path.normpath(
5180 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5181
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005182 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5183 # formatted. This is used to block during the presubmit.
5184 return_value = 0
5185
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005186 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005187 # Locate the clang-format binary in the checkout
5188 try:
5189 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005190 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005191 DieWithError(e)
5192
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005193 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005194 cmd = [clang_format_tool]
5195 if not opts.dry_run and not opts.diff:
5196 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005197 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005198 if opts.diff:
5199 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005200 else:
5201 env = os.environ.copy()
5202 env['PATH'] = str(os.path.dirname(clang_format_tool))
5203 try:
5204 script = clang_format.FindClangFormatScriptInChromiumTree(
5205 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005206 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005207 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005208
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005209 cmd = [sys.executable, script, '-p0']
5210 if not opts.dry_run and not opts.diff:
5211 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005212
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005213 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5214 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005215
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005216 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5217 if opts.diff:
5218 sys.stdout.write(stdout)
5219 if opts.dry_run and len(stdout) > 0:
5220 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005221
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005222 # Similar code to above, but using yapf on .py files rather than clang-format
5223 # on C/C++ files
5224 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005225 yapf_tool = gclient_utils.FindExecutable('yapf')
5226 if yapf_tool is None:
5227 DieWithError('yapf not found in PATH')
5228
5229 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005230 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005231 cmd = [yapf_tool]
5232 if not opts.dry_run and not opts.diff:
5233 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005234 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005235 if opts.diff:
5236 sys.stdout.write(stdout)
5237 else:
5238 # TODO(sbc): yapf --lines mode still has some issues.
5239 # https://github.com/google/yapf/issues/154
5240 DieWithError('--python currently only works with --full')
5241
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005242 # Dart's formatter does not have the nice property of only operating on
5243 # modified chunks, so hard code full.
5244 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005245 try:
5246 command = [dart_format.FindDartFmtToolInChromiumTree()]
5247 if not opts.dry_run and not opts.diff:
5248 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005249 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005250
ppi@chromium.org6593d932016-03-03 15:41:15 +00005251 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005252 if opts.dry_run and stdout:
5253 return_value = 2
5254 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005255 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5256 'found in this checkout. Files in other languages are still '
5257 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005258
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005259 # Format GN build files. Always run on full build files for canonical form.
5260 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005261 cmd = ['gn', 'format' ]
5262 if opts.dry_run or opts.diff:
5263 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005264 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005265 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5266 shell=sys.platform == 'win32',
5267 cwd=top_dir)
5268 if opts.dry_run and gn_ret == 2:
5269 return_value = 2 # Not formatted.
5270 elif opts.diff and gn_ret == 2:
5271 # TODO this should compute and print the actual diff.
5272 print("This change has GN build file diff for " + gn_diff_file)
5273 elif gn_ret != 0:
5274 # For non-dry run cases (and non-2 return values for dry-run), a
5275 # nonzero error code indicates a failure, probably because the file
5276 # doesn't parse.
5277 DieWithError("gn format failed on " + gn_diff_file +
5278 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005279
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005280 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005281
5282
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005283@subcommand.usage('<codereview url or issue id>')
5284def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005285 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005286 _, args = parser.parse_args(args)
5287
5288 if len(args) != 1:
5289 parser.print_help()
5290 return 1
5291
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005292 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005293 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005294 parser.print_help()
5295 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005296 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005297
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005298 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005299 output = RunGit(['config', '--local', '--get-regexp',
5300 r'branch\..*\.%s' % issueprefix],
5301 error_ok=True)
5302 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005303 if issue == target_issue:
5304 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005305
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005306 branches = []
5307 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005308 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005309 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005310 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005311 return 1
5312 if len(branches) == 1:
5313 RunGit(['checkout', branches[0]])
5314 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005315 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005316 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005317 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005318 which = raw_input('Choose by index: ')
5319 try:
5320 RunGit(['checkout', branches[int(which)]])
5321 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005322 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005323 return 1
5324
5325 return 0
5326
5327
maruel@chromium.org29404b52014-09-08 22:58:00 +00005328def CMDlol(parser, args):
5329 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005330 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005331 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5332 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5333 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005334 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005335 return 0
5336
5337
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005338class OptionParser(optparse.OptionParser):
5339 """Creates the option parse and add --verbose support."""
5340 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005341 optparse.OptionParser.__init__(
5342 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005343 self.add_option(
5344 '-v', '--verbose', action='count', default=0,
5345 help='Use 2 times for more debugging info')
5346
5347 def parse_args(self, args=None, values=None):
5348 options, args = optparse.OptionParser.parse_args(self, args, values)
5349 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5350 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5351 return options, args
5352
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005354def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005355 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005356 print('\nYour python version %s is unsupported, please upgrade.\n' %
5357 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005358 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005359
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005360 # Reload settings.
5361 global settings
5362 settings = Settings()
5363
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005364 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005365 dispatcher = subcommand.CommandDispatcher(__name__)
5366 try:
5367 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005368 except auth.AuthenticationError as e:
5369 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005370 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005371 if e.code != 500:
5372 raise
5373 DieWithError(
5374 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5375 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005376 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005377
5378
5379if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005380 # These affect sys.stdout so do it outside of main() to simplify mocks in
5381 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005382 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005383 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005384 try:
5385 sys.exit(main(sys.argv[1:]))
5386 except KeyboardInterrupt:
5387 sys.stderr.write('interrupted\n')
5388 sys.exit(1)