blob: c78207bdc9ee4f1ab1541ec1ca74238d8621b377 [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
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
87
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070089 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000090 sys.exit(1)
91
92
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000093def GetNoGitPagerEnv():
94 env = os.environ.copy()
95 # 'cat' is a magical git string that disables pagers on all platforms.
96 env['GIT_PAGER'] = 'cat'
97 return env
98
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000099
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000102 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000103 except subprocess2.CalledProcessError as e:
104 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 'Command "%s" failed.\n%s' % (
108 ' '.join(args), error_message or e.stdout or ''))
109 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
112def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000114 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115
116
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000118 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 try:
tandrii5d48c322016-08-18 16:19:37 -0700124 (out, _), code = subprocess2.communicate(['git'] + args,
125 env=GetNoGitPagerEnv(),
126 stdout=subprocess2.PIPE,
127 stderr=stderr)
128 return code, out
129 except subprocess2.CalledProcessError as e:
130 logging.debug('Failed running %s', args)
131 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000135 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
tandrii2a16b952016-10-19 07:09:44 -0700153def time_sleep(seconds):
154 # Use this so that it can be mocked in tests without interfering with python
155 # system machinery.
156 import time # Local import to discourage others from importing time globally.
157 return time.sleep(seconds)
158
159
maruel@chromium.org90541732011-04-01 17:54:18 +0000160def ask_for_data(prompt):
161 try:
162 return raw_input(prompt)
163 except KeyboardInterrupt:
164 # Hide the exception.
165 sys.exit(1)
166
167
tandrii5d48c322016-08-18 16:19:37 -0700168def _git_branch_config_key(branch, key):
169 """Helper method to return Git config key for a branch."""
170 assert branch, 'branch name is required to set git config for it'
171 return 'branch.%s.%s' % (branch, key)
172
173
174def _git_get_branch_config_value(key, default=None, value_type=str,
175 branch=False):
176 """Returns git config value of given or current branch if any.
177
178 Returns default in all other cases.
179 """
180 assert value_type in (int, str, bool)
181 if branch is False: # Distinguishing default arg value from None.
182 branch = GetCurrentBranch()
183
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000184 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700185 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000186
tandrii5d48c322016-08-18 16:19:37 -0700187 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700188 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700189 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700190 # git config also has --int, but apparently git config suffers from integer
191 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700192 args.append(_git_branch_config_key(branch, key))
193 code, out = RunGitWithCode(args)
194 if code == 0:
195 value = out.strip()
196 if value_type == int:
197 return int(value)
198 if value_type == bool:
199 return bool(value.lower() == 'true')
200 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 return default
202
203
tandrii5d48c322016-08-18 16:19:37 -0700204def _git_set_branch_config_value(key, value, branch=None, **kwargs):
205 """Sets the value or unsets if it's None of a git branch config.
206
207 Valid, though not necessarily existing, branch must be provided,
208 otherwise currently checked out branch is used.
209 """
210 if not branch:
211 branch = GetCurrentBranch()
212 assert branch, 'a branch name OR currently checked out branch is required'
213 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700214 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700215 if value is None:
216 args.append('--unset')
217 elif isinstance(value, bool):
218 args.append('--bool')
219 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700220 else:
tandrii33a46ff2016-08-23 05:53:40 -0700221 # git config also has --int, but apparently git config suffers from integer
222 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700223 value = str(value)
224 args.append(_git_branch_config_key(branch, key))
225 if value is not None:
226 args.append(value)
227 RunGit(args, **kwargs)
228
229
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000230def add_git_similarity(parser):
231 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700232 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000233 help='Sets the percentage that a pair of files need to match in order to'
234 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000235 parser.add_option(
236 '--find-copies', action='store_true',
237 help='Allows git to look for copies.')
238 parser.add_option(
239 '--no-find-copies', action='store_false', dest='find_copies',
240 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000241
242 old_parser_args = parser.parse_args
243 def Parse(args):
244 options, args = old_parser_args(args)
245
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000246 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700247 options.similarity = _git_get_branch_config_value(
248 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000249 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 print('Note: Saving similarity of %d%% in git config.'
251 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700252 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000253
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254 options.similarity = max(0, min(options.similarity, 100))
255
256 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700257 options.find_copies = _git_get_branch_config_value(
258 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000259 else:
tandrii5d48c322016-08-18 16:19:37 -0700260 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000261
262 print('Using %d%% similarity for rename/copy detection. '
263 'Override with --similarity.' % options.similarity)
264
265 return options, args
266 parser.parse_args = Parse
267
268
machenbach@chromium.org45453142015-09-15 08:45:22 +0000269def _get_properties_from_options(options):
270 properties = dict(x.split('=', 1) for x in options.properties)
271 for key, val in properties.iteritems():
272 try:
273 properties[key] = json.loads(val)
274 except ValueError:
275 pass # If a value couldn't be evaluated, treat it as a string.
276 return properties
277
278
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000279def _prefix_master(master):
280 """Convert user-specified master name to full master name.
281
282 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
283 name, while the developers always use shortened master name
284 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
285 function does the conversion for buildbucket migration.
286 """
borenet6c0efe62016-10-19 08:13:29 -0700287 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000288 return master
borenet6c0efe62016-10-19 08:13:29 -0700289 return '%s%s' % (MASTER_PREFIX, master)
290
291
292def _unprefix_master(bucket):
293 """Convert bucket name to shortened master name.
294
295 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
296 name, while the developers always use shortened master name
297 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
298 function does the conversion for buildbucket migration.
299 """
300 if bucket.startswith(MASTER_PREFIX):
301 return bucket[len(MASTER_PREFIX):]
302 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000303
304
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000305def _buildbucket_retry(operation_name, http, *args, **kwargs):
306 """Retries requests to buildbucket service and returns parsed json content."""
307 try_count = 0
308 while True:
309 response, content = http.request(*args, **kwargs)
310 try:
311 content_json = json.loads(content)
312 except ValueError:
313 content_json = None
314
315 # Buildbucket could return an error even if status==200.
316 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000317 error = content_json.get('error')
318 if error.get('code') == 403:
319 raise BuildbucketResponseException(
320 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000321 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000322 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000323 raise BuildbucketResponseException(msg)
324
325 if response.status == 200:
326 if not content_json:
327 raise BuildbucketResponseException(
328 'Buildbucket returns invalid json content: %s.\n'
329 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
330 content)
331 return content_json
332 if response.status < 500 or try_count >= 2:
333 raise httplib2.HttpLib2Error(content)
334
335 # status >= 500 means transient failures.
336 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700337 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000338 try_count += 1
339 assert False, 'unreachable'
340
341
borenet6c0efe62016-10-19 08:13:29 -0700342def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700343 category='git_cl_try', patchset=None):
344 assert changelist.GetIssue(), 'CL must be uploaded first'
345 codereview_url = changelist.GetCodereviewServer()
346 assert codereview_url, 'CL must be uploaded first'
347 patchset = patchset or changelist.GetMostRecentPatchset()
348 assert patchset, 'CL must be uploaded first'
349
350 codereview_host = urlparse.urlparse(codereview_url).hostname
351 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000352 http = authenticator.authorize(httplib2.Http())
353 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700354
355 # TODO(tandrii): consider caching Gerrit CL details just like
356 # _RietveldChangelistImpl does, then caching values in these two variables
357 # won't be necessary.
358 owner_email = changelist.GetIssueOwner()
359 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000360
361 buildbucket_put_url = (
362 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000363 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700364 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
365 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
366 hostname=codereview_host,
367 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700369 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
371 batch_req_body = {'builds': []}
372 print_text = []
373 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700374 for bucket, builders_and_tests in sorted(buckets.iteritems()):
375 print_text.append('Bucket: %s' % bucket)
376 master = None
377 if bucket.startswith(MASTER_PREFIX):
378 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000379 for builder, tests in sorted(builders_and_tests.iteritems()):
380 print_text.append(' %s: %s' % (builder, tests))
381 parameters = {
382 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000383 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700384 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000385 'revision': options.revision,
386 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000387 'properties': {
388 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700389 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700390 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000391 'patch_storage': 'rietveld',
392 'patchset': patchset,
393 'reason': options.name,
tandriide281ae2016-10-12 06:02:30 -0700394 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000395 },
396 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000397 if 'presubmit' in builder.lower():
398 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000399 if tests:
400 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700401 if extra_properties:
402 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000403 if options.clobber:
404 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700405
406 tags = [
407 'builder:%s' % builder,
408 'buildset:%s' % buildset,
409 'user_agent:git_cl_try',
410 ]
411 if master:
412 parameters['properties']['master'] = master
413 tags.append('master:%s' % master)
414
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000415 batch_req_body['builds'].append(
416 {
417 'bucket': bucket,
418 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700420 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000421 }
422 )
423
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000424 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700425 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000426 http,
427 buildbucket_put_url,
428 'PUT',
429 body=json.dumps(batch_req_body),
430 headers={'Content-Type': 'application/json'}
431 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000432 print_text.append('To see results here, run: git cl try-results')
433 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700434 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000435
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000436
tandrii221ab252016-10-06 08:12:04 -0700437def fetch_try_jobs(auth_config, changelist, buildbucket_host,
438 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700439 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000440
qyearsley53f48a12016-09-01 10:45:13 -0700441 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000442 """
tandrii221ab252016-10-06 08:12:04 -0700443 assert buildbucket_host
444 assert changelist.GetIssue(), 'CL must be uploaded first'
445 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
446 patchset = patchset or changelist.GetMostRecentPatchset()
447 assert patchset, 'CL must be uploaded first'
448
449 codereview_url = changelist.GetCodereviewServer()
450 codereview_host = urlparse.urlparse(codereview_url).hostname
451 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000452 if authenticator.has_cached_credentials():
453 http = authenticator.authorize(httplib2.Http())
454 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700455 print('Warning: Some results might be missing because %s' %
456 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700457 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 http = httplib2.Http()
459
460 http.force_exception_to_status_code = True
461
tandrii221ab252016-10-06 08:12:04 -0700462 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
463 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
464 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000465 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700466 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000467 params = {'tag': 'buildset:%s' % buildset}
468
469 builds = {}
470 while True:
471 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700472 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000473 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700474 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475 for build in content.get('builds', []):
476 builds[build['id']] = build
477 if 'next_cursor' in content:
478 params['start_cursor'] = content['next_cursor']
479 else:
480 break
481 return builds
482
483
qyearsleyeab3c042016-08-24 09:18:28 -0700484def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000485 """Prints nicely result of fetch_try_jobs."""
486 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700487 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000488 return
489
490 # Make a copy, because we'll be modifying builds dictionary.
491 builds = builds.copy()
492 builder_names_cache = {}
493
494 def get_builder(b):
495 try:
496 return builder_names_cache[b['id']]
497 except KeyError:
498 try:
499 parameters = json.loads(b['parameters_json'])
500 name = parameters['builder_name']
501 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700502 print('WARNING: failed to get builder name for build %s: %s' % (
503 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 name = None
505 builder_names_cache[b['id']] = name
506 return name
507
508 def get_bucket(b):
509 bucket = b['bucket']
510 if bucket.startswith('master.'):
511 return bucket[len('master.'):]
512 return bucket
513
514 if options.print_master:
515 name_fmt = '%%-%ds %%-%ds' % (
516 max(len(str(get_bucket(b))) for b in builds.itervalues()),
517 max(len(str(get_builder(b))) for b in builds.itervalues()))
518 def get_name(b):
519 return name_fmt % (get_bucket(b), get_builder(b))
520 else:
521 name_fmt = '%%-%ds' % (
522 max(len(str(get_builder(b))) for b in builds.itervalues()))
523 def get_name(b):
524 return name_fmt % get_builder(b)
525
526 def sort_key(b):
527 return b['status'], b.get('result'), get_name(b), b.get('url')
528
529 def pop(title, f, color=None, **kwargs):
530 """Pop matching builds from `builds` dict and print them."""
531
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000532 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 colorize = str
534 else:
535 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
536
537 result = []
538 for b in builds.values():
539 if all(b.get(k) == v for k, v in kwargs.iteritems()):
540 builds.pop(b['id'])
541 result.append(b)
542 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700543 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700545 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546
547 total = len(builds)
548 pop(status='COMPLETED', result='SUCCESS',
549 title='Successes:', color=Fore.GREEN,
550 f=lambda b: (get_name(b), b.get('url')))
551 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
552 title='Infra Failures:', color=Fore.MAGENTA,
553 f=lambda b: (get_name(b), b.get('url')))
554 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
555 title='Failures:', color=Fore.RED,
556 f=lambda b: (get_name(b), b.get('url')))
557 pop(status='COMPLETED', result='CANCELED',
558 title='Canceled:', color=Fore.MAGENTA,
559 f=lambda b: (get_name(b),))
560 pop(status='COMPLETED', result='FAILURE',
561 failure_reason='INVALID_BUILD_DEFINITION',
562 title='Wrong master/builder name:', color=Fore.MAGENTA,
563 f=lambda b: (get_name(b),))
564 pop(status='COMPLETED', result='FAILURE',
565 title='Other failures:',
566 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
567 pop(status='COMPLETED',
568 title='Other finished:',
569 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
570 pop(status='STARTED',
571 title='Started:', color=Fore.YELLOW,
572 f=lambda b: (get_name(b), b.get('url')))
573 pop(status='SCHEDULED',
574 title='Scheduled:',
575 f=lambda b: (get_name(b), 'id=%s' % b['id']))
576 # The last section is just in case buildbucket API changes OR there is a bug.
577 pop(title='Other:',
578 f=lambda b: (get_name(b), 'id=%s' % b['id']))
579 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700580 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581
582
qyearsley53f48a12016-09-01 10:45:13 -0700583def write_try_results_json(output_file, builds):
584 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
585
586 The input |builds| dict is assumed to be generated by Buildbucket.
587 Buildbucket documentation: http://goo.gl/G0s101
588 """
589
590 def convert_build_dict(build):
591 return {
592 'buildbucket_id': build.get('id'),
593 'status': build.get('status'),
594 'result': build.get('result'),
595 'bucket': build.get('bucket'),
596 'builder_name': json.loads(
597 build.get('parameters_json', '{}')).get('builder_name'),
598 'failure_reason': build.get('failure_reason'),
599 'url': build.get('url'),
600 }
601
602 converted = []
603 for _, build in sorted(builds.items()):
604 converted.append(convert_build_dict(build))
605 write_json(output_file, converted)
606
607
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000608def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
609 """Return the corresponding git ref if |base_url| together with |glob_spec|
610 matches the full |url|.
611
612 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
613 """
614 fetch_suburl, as_ref = glob_spec.split(':')
615 if allow_wildcards:
616 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
617 if glob_match:
618 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
619 # "branches/{472,597,648}/src:refs/remotes/svn/*".
620 branch_re = re.escape(base_url)
621 if glob_match.group(1):
622 branch_re += '/' + re.escape(glob_match.group(1))
623 wildcard = glob_match.group(2)
624 if wildcard == '*':
625 branch_re += '([^/]*)'
626 else:
627 # Escape and replace surrounding braces with parentheses and commas
628 # with pipe symbols.
629 wildcard = re.escape(wildcard)
630 wildcard = re.sub('^\\\\{', '(', wildcard)
631 wildcard = re.sub('\\\\,', '|', wildcard)
632 wildcard = re.sub('\\\\}$', ')', wildcard)
633 branch_re += wildcard
634 if glob_match.group(3):
635 branch_re += re.escape(glob_match.group(3))
636 match = re.match(branch_re, url)
637 if match:
638 return re.sub('\*$', match.group(1), as_ref)
639
640 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
641 if fetch_suburl:
642 full_url = base_url + '/' + fetch_suburl
643 else:
644 full_url = base_url
645 if full_url == url:
646 return as_ref
647 return None
648
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000649
iannucci@chromium.org79540052012-10-19 23:15:26 +0000650def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000651 """Prints statistics about the change to the user."""
652 # --no-ext-diff is broken in some versions of Git, so try to work around
653 # this by overriding the environment (but there is still a problem if the
654 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000655 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000656 if 'GIT_EXTERNAL_DIFF' in env:
657 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000658
659 if find_copies:
660 similarity_options = ['--find-copies-harder', '-l100000',
661 '-C%s' % similarity]
662 else:
663 similarity_options = ['-M%s' % similarity]
664
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000665 try:
666 stdout = sys.stdout.fileno()
667 except AttributeError:
668 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000669 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000670 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000671 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000672 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000673
674
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000675class BuildbucketResponseException(Exception):
676 pass
677
678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679class Settings(object):
680 def __init__(self):
681 self.default_server = None
682 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000683 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684 self.is_git_svn = None
685 self.svn_branch = None
686 self.tree_status_url = None
687 self.viewvc_url = None
688 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000689 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000690 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000691 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000692 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000693 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000694 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000695 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700696 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697
698 def LazyUpdateIfNeeded(self):
699 """Updates the settings from a codereview.settings file, if available."""
700 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000701 # The only value that actually changes the behavior is
702 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000703 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000704 error_ok=True
705 ).strip().lower()
706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000708 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 LoadCodereviewSettingsFromFile(cr_settings_file)
710 self.updated = True
711
712 def GetDefaultServerUrl(self, error_ok=False):
713 if not self.default_server:
714 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000715 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000716 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 if error_ok:
718 return self.default_server
719 if not self.default_server:
720 error_message = ('Could not find settings file. You must configure '
721 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000722 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 return self.default_server
725
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000726 @staticmethod
727 def GetRelativeRoot():
728 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000731 if self.root is None:
732 self.root = os.path.abspath(self.GetRelativeRoot())
733 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000735 def GetGitMirror(self, remote='origin'):
736 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000737 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000738 if not os.path.isdir(local_url):
739 return None
740 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
741 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
742 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
743 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
744 if mirror.exists():
745 return mirror
746 return None
747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 def GetIsGitSvn(self):
749 """Return true if this repo looks like it's using git-svn."""
750 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000751 if self.GetPendingRefPrefix():
752 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
753 self.is_git_svn = False
754 else:
755 # If you have any "svn-remote.*" config keys, we think you're using svn.
756 self.is_git_svn = RunGitWithCode(
757 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 return self.is_git_svn
759
760 def GetSVNBranch(self):
761 if self.svn_branch is None:
762 if not self.GetIsGitSvn():
763 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
764
765 # Try to figure out which remote branch we're based on.
766 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000767 # 1) iterate through our branch history and find the svn URL.
768 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769
770 # regexp matching the git-svn line that contains the URL.
771 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
772
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000773 # We don't want to go through all of history, so read a line from the
774 # pipe at a time.
775 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000776 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000777 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
778 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000779 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000780 for line in proc.stdout:
781 match = git_svn_re.match(line)
782 if match:
783 url = match.group(1)
784 proc.stdout.close() # Cut pipe.
785 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000787 if url:
788 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
789 remotes = RunGit(['config', '--get-regexp',
790 r'^svn-remote\..*\.url']).splitlines()
791 for remote in remotes:
792 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000794 remote = match.group(1)
795 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000796 rewrite_root = RunGit(
797 ['config', 'svn-remote.%s.rewriteRoot' % remote],
798 error_ok=True).strip()
799 if rewrite_root:
800 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000801 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000802 ['config', 'svn-remote.%s.fetch' % remote],
803 error_ok=True).strip()
804 if fetch_spec:
805 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
806 if self.svn_branch:
807 break
808 branch_spec = RunGit(
809 ['config', 'svn-remote.%s.branches' % remote],
810 error_ok=True).strip()
811 if branch_spec:
812 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
813 if self.svn_branch:
814 break
815 tag_spec = RunGit(
816 ['config', 'svn-remote.%s.tags' % remote],
817 error_ok=True).strip()
818 if tag_spec:
819 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
820 if self.svn_branch:
821 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
823 if not self.svn_branch:
824 DieWithError('Can\'t guess svn branch -- try specifying it on the '
825 'command line')
826
827 return self.svn_branch
828
829 def GetTreeStatusUrl(self, error_ok=False):
830 if not self.tree_status_url:
831 error_message = ('You must configure your tree status URL by running '
832 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 self.tree_status_url = self._GetRietveldConfig(
834 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835 return self.tree_status_url
836
837 def GetViewVCUrl(self):
838 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000839 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 return self.viewvc_url
841
rmistry@google.com90752582014-01-14 21:04:50 +0000842 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000843 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000844
rmistry@google.com78948ed2015-07-08 23:09:57 +0000845 def GetIsSkipDependencyUpload(self, branch_name):
846 """Returns true if specified branch should skip dep uploads."""
847 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
848 error_ok=True)
849
rmistry@google.com5626a922015-02-26 14:03:30 +0000850 def GetRunPostUploadHook(self):
851 run_post_upload_hook = self._GetRietveldConfig(
852 'run-post-upload-hook', error_ok=True)
853 return run_post_upload_hook == "True"
854
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000855 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000856 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000857
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000858 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000859 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000860
ukai@chromium.orge8077812012-02-03 03:41:46 +0000861 def GetIsGerrit(self):
862 """Return true if this repo is assosiated with gerrit code review system."""
863 if self.is_gerrit is None:
864 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
865 return self.is_gerrit
866
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000867 def GetSquashGerritUploads(self):
868 """Return true if uploads to Gerrit should be squashed by default."""
869 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700870 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
871 if self.squash_gerrit_uploads is None:
872 # Default is squash now (http://crbug.com/611892#c23).
873 self.squash_gerrit_uploads = not (
874 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
875 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000876 return self.squash_gerrit_uploads
877
tandriia60502f2016-06-20 02:01:53 -0700878 def GetSquashGerritUploadsOverride(self):
879 """Return True or False if codereview.settings should be overridden.
880
881 Returns None if no override has been defined.
882 """
883 # See also http://crbug.com/611892#c23
884 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
885 error_ok=True).strip()
886 if result == 'true':
887 return True
888 if result == 'false':
889 return False
890 return None
891
tandrii@chromium.org28253532016-04-14 13:46:56 +0000892 def GetGerritSkipEnsureAuthenticated(self):
893 """Return True if EnsureAuthenticated should not be done for Gerrit
894 uploads."""
895 if self.gerrit_skip_ensure_authenticated is None:
896 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000897 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000898 error_ok=True).strip() == 'true')
899 return self.gerrit_skip_ensure_authenticated
900
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000901 def GetGitEditor(self):
902 """Return the editor specified in the git config, or None if none is."""
903 if self.git_editor is None:
904 self.git_editor = self._GetConfig('core.editor', error_ok=True)
905 return self.git_editor or None
906
thestig@chromium.org44202a22014-03-11 19:22:18 +0000907 def GetLintRegex(self):
908 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
909 DEFAULT_LINT_REGEX)
910
911 def GetLintIgnoreRegex(self):
912 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
913 DEFAULT_LINT_IGNORE_REGEX)
914
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000915 def GetProject(self):
916 if not self.project:
917 self.project = self._GetRietveldConfig('project', error_ok=True)
918 return self.project
919
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000920 def GetForceHttpsCommitUrl(self):
921 if not self.force_https_commit_url:
922 self.force_https_commit_url = self._GetRietveldConfig(
923 'force-https-commit-url', error_ok=True)
924 return self.force_https_commit_url
925
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000926 def GetPendingRefPrefix(self):
927 if not self.pending_ref_prefix:
928 self.pending_ref_prefix = self._GetRietveldConfig(
929 'pending-ref-prefix', error_ok=True)
930 return self.pending_ref_prefix
931
tandriif46c20f2016-09-14 06:17:05 -0700932 def GetHasGitNumberFooter(self):
933 # TODO(tandrii): this has to be removed after Rietveld is read-only.
934 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
935 if not self.git_number_footer:
936 self.git_number_footer = self._GetRietveldConfig(
937 'git-number-footer', error_ok=True)
938 return self.git_number_footer
939
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000940 def _GetRietveldConfig(self, param, **kwargs):
941 return self._GetConfig('rietveld.' + param, **kwargs)
942
rmistry@google.com78948ed2015-07-08 23:09:57 +0000943 def _GetBranchConfig(self, branch_name, param, **kwargs):
944 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 def _GetConfig(self, param, **kwargs):
947 self.LazyUpdateIfNeeded()
948 return RunGit(['config', param], **kwargs).strip()
949
950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951def ShortBranchName(branch):
952 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000953 return branch.replace('refs/heads/', '', 1)
954
955
956def GetCurrentBranchRef():
957 """Returns branch ref (e.g., refs/heads/master) or None."""
958 return RunGit(['symbolic-ref', 'HEAD'],
959 stderr=subprocess2.VOID, error_ok=True).strip() or None
960
961
962def GetCurrentBranch():
963 """Returns current branch or None.
964
965 For refs/heads/* branches, returns just last part. For others, full ref.
966 """
967 branchref = GetCurrentBranchRef()
968 if branchref:
969 return ShortBranchName(branchref)
970 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971
972
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000973class _CQState(object):
974 """Enum for states of CL with respect to Commit Queue."""
975 NONE = 'none'
976 DRY_RUN = 'dry_run'
977 COMMIT = 'commit'
978
979 ALL_STATES = [NONE, DRY_RUN, COMMIT]
980
981
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000982class _ParsedIssueNumberArgument(object):
983 def __init__(self, issue=None, patchset=None, hostname=None):
984 self.issue = issue
985 self.patchset = patchset
986 self.hostname = hostname
987
988 @property
989 def valid(self):
990 return self.issue is not None
991
992
993class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
994 def __init__(self, *args, **kwargs):
995 self.patch_url = kwargs.pop('patch_url', None)
996 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
997
998
999def ParseIssueNumberArgument(arg):
1000 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1001 fail_result = _ParsedIssueNumberArgument()
1002
1003 if arg.isdigit():
1004 return _ParsedIssueNumberArgument(issue=int(arg))
1005 if not arg.startswith('http'):
1006 return fail_result
1007 url = gclient_utils.UpgradeToHttps(arg)
1008 try:
1009 parsed_url = urlparse.urlparse(url)
1010 except ValueError:
1011 return fail_result
1012 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1013 tmp = cls.ParseIssueURL(parsed_url)
1014 if tmp is not None:
1015 return tmp
1016 return fail_result
1017
1018
tandriic2405f52016-10-10 08:13:15 -07001019class GerritIssueNotExists(Exception):
1020 def __init__(self, issue, url):
1021 self.issue = issue
1022 self.url = url
1023 super(GerritIssueNotExists, self).__init__()
1024
1025 def __str__(self):
1026 return 'issue %s at %s does not exist or you have no access to it' % (
1027 self.issue, self.url)
1028
1029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 """Changelist works with one changelist in local branch.
1032
1033 Supports two codereview backends: Rietveld or Gerrit, selected at object
1034 creation.
1035
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001036 Notes:
1037 * Not safe for concurrent multi-{thread,process} use.
1038 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001039 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001040 """
1041
1042 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1043 """Create a new ChangeList instance.
1044
1045 If issue is given, the codereview must be given too.
1046
1047 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1048 Otherwise, it's decided based on current configuration of the local branch,
1049 with default being 'rietveld' for backwards compatibility.
1050 See _load_codereview_impl for more details.
1051
1052 **kwargs will be passed directly to codereview implementation.
1053 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001055 global settings
1056 if not settings:
1057 # Happens when git_cl.py is used as a utility library.
1058 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001059
1060 if issue:
1061 assert codereview, 'codereview must be known, if issue is known'
1062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063 self.branchref = branchref
1064 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001065 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 self.branch = ShortBranchName(self.branchref)
1067 else:
1068 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001070 self.lookedup_issue = False
1071 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 self.has_description = False
1073 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001074 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001076 self.cc = None
1077 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001078 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001079
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001080 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001081 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001082 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001083 assert self._codereview_impl
1084 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001085
1086 def _load_codereview_impl(self, codereview=None, **kwargs):
1087 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001088 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1089 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1090 self._codereview = codereview
1091 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001092 return
1093
1094 # Automatic selection based on issue number set for a current branch.
1095 # Rietveld takes precedence over Gerrit.
1096 assert not self.issue
1097 # Whether we find issue or not, we are doing the lookup.
1098 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001099 if self.GetBranch():
1100 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1101 issue = _git_get_branch_config_value(
1102 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1103 if issue:
1104 self._codereview = codereview
1105 self._codereview_impl = cls(self, **kwargs)
1106 self.issue = int(issue)
1107 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001108
1109 # No issue is set for this branch, so decide based on repo-wide settings.
1110 return self._load_codereview_impl(
1111 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1112 **kwargs)
1113
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001114 def IsGerrit(self):
1115 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001116
1117 def GetCCList(self):
1118 """Return the users cc'd on this CL.
1119
agable92bec4f2016-08-24 09:27:27 -07001120 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001121 """
1122 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001123 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001124 more_cc = ','.join(self.watchers)
1125 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1126 return self.cc
1127
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001128 def GetCCListWithoutDefault(self):
1129 """Return the users cc'd on this CL excluding default ones."""
1130 if self.cc is None:
1131 self.cc = ','.join(self.watchers)
1132 return self.cc
1133
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001134 def SetWatchers(self, watchers):
1135 """Set the list of email addresses that should be cc'd based on the changed
1136 files in this CL.
1137 """
1138 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139
1140 def GetBranch(self):
1141 """Returns the short branch name, e.g. 'master'."""
1142 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001144 if not branchref:
1145 return None
1146 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.branch = ShortBranchName(self.branchref)
1148 return self.branch
1149
1150 def GetBranchRef(self):
1151 """Returns the full branch name, e.g. 'refs/heads/master'."""
1152 self.GetBranch() # Poke the lazy loader.
1153 return self.branchref
1154
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001155 def ClearBranch(self):
1156 """Clears cached branch data of this object."""
1157 self.branch = self.branchref = None
1158
tandrii5d48c322016-08-18 16:19:37 -07001159 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1160 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1161 kwargs['branch'] = self.GetBranch()
1162 return _git_get_branch_config_value(key, default, **kwargs)
1163
1164 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1165 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1166 assert self.GetBranch(), (
1167 'this CL must have an associated branch to %sset %s%s' %
1168 ('un' if value is None else '',
1169 key,
1170 '' if value is None else ' to %r' % value))
1171 kwargs['branch'] = self.GetBranch()
1172 return _git_set_branch_config_value(key, value, **kwargs)
1173
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001174 @staticmethod
1175 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001176 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 e.g. 'origin', 'refs/heads/master'
1178 """
1179 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001180 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001183 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001185 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1186 error_ok=True).strip()
1187 if upstream_branch:
1188 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001190 # Fall back on trying a git-svn upstream branch.
1191 if settings.GetIsGitSvn():
1192 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001194 # Else, try to guess the origin remote.
1195 remote_branches = RunGit(['branch', '-r']).split()
1196 if 'origin/master' in remote_branches:
1197 # Fall back on origin/master if it exits.
1198 remote = 'origin'
1199 upstream_branch = 'refs/heads/master'
1200 elif 'origin/trunk' in remote_branches:
1201 # Fall back on origin/trunk if it exists. Generally a shared
1202 # git-svn clone
1203 remote = 'origin'
1204 upstream_branch = 'refs/heads/trunk'
1205 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 DieWithError(
1207 'Unable to determine default branch to diff against.\n'
1208 'Either pass complete "git diff"-style arguments, like\n'
1209 ' git cl upload origin/master\n'
1210 'or verify this branch is set up to track another \n'
1211 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212
1213 return remote, upstream_branch
1214
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001215 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001216 upstream_branch = self.GetUpstreamBranch()
1217 if not BranchExists(upstream_branch):
1218 DieWithError('The upstream for the current branch (%s) does not exist '
1219 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001220 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001221 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001222
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 def GetUpstreamBranch(self):
1224 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001225 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001227 upstream_branch = upstream_branch.replace('refs/heads/',
1228 'refs/remotes/%s/' % remote)
1229 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1230 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 self.upstream_branch = upstream_branch
1232 return self.upstream_branch
1233
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001234 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001235 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001236 remote, branch = None, self.GetBranch()
1237 seen_branches = set()
1238 while branch not in seen_branches:
1239 seen_branches.add(branch)
1240 remote, branch = self.FetchUpstreamTuple(branch)
1241 branch = ShortBranchName(branch)
1242 if remote != '.' or branch.startswith('refs/remotes'):
1243 break
1244 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001245 remotes = RunGit(['remote'], error_ok=True).split()
1246 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001247 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001248 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001250 logging.warning('Could not determine which remote this change is '
1251 'associated with, so defaulting to "%s". This may '
1252 'not be what you want. You may prevent this message '
1253 'by running "git svn info" as documented here: %s',
1254 self._remote,
1255 GIT_INSTRUCTIONS_URL)
1256 else:
1257 logging.warn('Could not determine which remote this change is '
1258 'associated with. You may prevent this message by '
1259 'running "git svn info" as documented here: %s',
1260 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 branch = 'HEAD'
1262 if branch.startswith('refs/remotes'):
1263 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001264 elif branch.startswith('refs/branch-heads/'):
1265 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001266 else:
1267 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001268 return self._remote
1269
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 def GitSanityChecks(self, upstream_git_obj):
1271 """Checks git repo status and ensures diff is from local commits."""
1272
sbc@chromium.org79706062015-01-14 21:18:12 +00001273 if upstream_git_obj is None:
1274 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001275 print('ERROR: unable to determine current branch (detached HEAD?)',
1276 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001277 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001278 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001279 return False
1280
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 # Verify the commit we're diffing against is in our current branch.
1282 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1283 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1284 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001285 print('ERROR: %s is not in the current branch. You may need to rebase '
1286 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 return False
1288
1289 # List the commits inside the diff, and verify they are all local.
1290 commits_in_diff = RunGit(
1291 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1292 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1293 remote_branch = remote_branch.strip()
1294 if code != 0:
1295 _, remote_branch = self.GetRemoteBranch()
1296
1297 commits_in_remote = RunGit(
1298 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1299
1300 common_commits = set(commits_in_diff) & set(commits_in_remote)
1301 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001302 print('ERROR: Your diff contains %d commits already in %s.\n'
1303 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1304 'the diff. If you are using a custom git flow, you can override'
1305 ' the reference used for this check with "git config '
1306 'gitcl.remotebranch <git-ref>".' % (
1307 len(common_commits), remote_branch, upstream_git_obj),
1308 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 return False
1310 return True
1311
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001312 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001313 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001314
1315 Returns None if it is not set.
1316 """
tandrii5d48c322016-08-18 16:19:37 -07001317 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001318
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001319 def GetGitSvnRemoteUrl(self):
1320 """Return the configured git-svn remote URL parsed from git svn info.
1321
1322 Returns None if it is not set.
1323 """
1324 # URL is dependent on the current directory.
1325 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1326 if data:
1327 keys = dict(line.split(': ', 1) for line in data.splitlines()
1328 if ': ' in line)
1329 return keys.get('URL', None)
1330 return None
1331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 def GetRemoteUrl(self):
1333 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1334
1335 Returns None if there is no remote.
1336 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001338 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1339
1340 # If URL is pointing to a local directory, it is probably a git cache.
1341 if os.path.isdir(url):
1342 url = RunGit(['config', 'remote.%s.url' % remote],
1343 error_ok=True,
1344 cwd=url).strip()
1345 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001347 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001348 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001349 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001350 self.issue = self._GitGetBranchConfigValue(
1351 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001352 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 return self.issue
1354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 def GetIssueURL(self):
1356 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001357 issue = self.GetIssue()
1358 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001359 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
1362 def GetDescription(self, pretty=False):
1363 if not self.has_description:
1364 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001365 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 self.has_description = True
1367 if pretty:
1368 wrapper = textwrap.TextWrapper()
1369 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1370 return wrapper.fill(self.description)
1371 return self.description
1372
1373 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001374 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001375 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001376 self.patchset = self._GitGetBranchConfigValue(
1377 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return self.patchset
1380
1381 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001382 """Set this branch's patchset. If patchset=0, clears the patchset."""
1383 assert self.GetBranch()
1384 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001385 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001386 else:
1387 self.patchset = int(patchset)
1388 self._GitSetBranchConfigValue(
1389 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001391 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001392 """Set this branch's issue. If issue isn't given, clears the issue."""
1393 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001395 issue = int(issue)
1396 self._GitSetBranchConfigValue(
1397 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001399 codereview_server = self._codereview_impl.GetCodereviewServer()
1400 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001401 self._GitSetBranchConfigValue(
1402 self._codereview_impl.CodereviewServerConfigKey(),
1403 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 else:
tandrii5d48c322016-08-18 16:19:37 -07001405 # Reset all of these just to be clean.
1406 reset_suffixes = [
1407 'last-upload-hash',
1408 self._codereview_impl.IssueConfigKey(),
1409 self._codereview_impl.PatchsetConfigKey(),
1410 self._codereview_impl.CodereviewServerConfigKey(),
1411 ] + self._PostUnsetIssueProperties()
1412 for prop in reset_suffixes:
1413 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001415 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
dnjba1b0f32016-09-02 12:37:42 -07001417 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001418 if not self.GitSanityChecks(upstream_branch):
1419 DieWithError('\nGit sanity check failure')
1420
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001421 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001422 if not root:
1423 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001424 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001425
1426 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001427 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001428 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001429 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001430 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001431 except subprocess2.CalledProcessError:
1432 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001433 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001434 'This branch probably doesn\'t exist anymore. To reset the\n'
1435 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001436 ' git branch --set-upstream-to origin/master %s\n'
1437 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001438 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001439
maruel@chromium.org52424302012-08-29 15:14:30 +00001440 issue = self.GetIssue()
1441 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001442 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001443 description = self.GetDescription()
1444 else:
1445 # If the change was never uploaded, use the log messages of all commits
1446 # up to the branch point, as git cl upload will prefill the description
1447 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001448 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1449 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001450
1451 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001452 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001453 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001454 name,
1455 description,
1456 absroot,
1457 files,
1458 issue,
1459 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001460 author,
1461 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001462
dsansomee2d6fd92016-09-08 00:10:47 -07001463 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001465 return self._codereview_impl.UpdateDescriptionRemote(
1466 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467
1468 def RunHook(self, committing, may_prompt, verbose, change):
1469 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1470 try:
1471 return presubmit_support.DoPresubmitChecks(change, committing,
1472 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1473 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001474 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1475 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001476 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001477 DieWithError(
1478 ('%s\nMaybe your depot_tools is out of date?\n'
1479 'If all fails, contact maruel@') % e)
1480
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001481 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1482 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001483 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1484 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001485 else:
1486 # Assume url.
1487 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1488 urlparse.urlparse(issue_arg))
1489 if not parsed_issue_arg or not parsed_issue_arg.valid:
1490 DieWithError('Failed to parse issue argument "%s". '
1491 'Must be an issue number or a valid URL.' % issue_arg)
1492 return self._codereview_impl.CMDPatchWithParsedIssue(
1493 parsed_issue_arg, reject, nocommit, directory)
1494
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001495 def CMDUpload(self, options, git_diff_args, orig_args):
1496 """Uploads a change to codereview."""
1497 if git_diff_args:
1498 # TODO(ukai): is it ok for gerrit case?
1499 base_branch = git_diff_args[0]
1500 else:
1501 if self.GetBranch() is None:
1502 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1503
1504 # Default to diffing against common ancestor of upstream branch
1505 base_branch = self.GetCommonAncestorWithUpstream()
1506 git_diff_args = [base_branch, 'HEAD']
1507
1508 # Make sure authenticated to codereview before running potentially expensive
1509 # hooks. It is a fast, best efforts check. Codereview still can reject the
1510 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001511 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001512
1513 # Apply watchlists on upload.
1514 change = self.GetChange(base_branch, None)
1515 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1516 files = [f.LocalPath() for f in change.AffectedFiles()]
1517 if not options.bypass_watchlists:
1518 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1519
1520 if not options.bypass_hooks:
1521 if options.reviewers or options.tbr_owners:
1522 # Set the reviewer list now so that presubmit checks can access it.
1523 change_description = ChangeDescription(change.FullDescriptionText())
1524 change_description.update_reviewers(options.reviewers,
1525 options.tbr_owners,
1526 change)
1527 change.SetDescriptionText(change_description.description)
1528 hook_results = self.RunHook(committing=False,
1529 may_prompt=not options.force,
1530 verbose=options.verbose,
1531 change=change)
1532 if not hook_results.should_continue():
1533 return 1
1534 if not options.reviewers and hook_results.reviewers:
1535 options.reviewers = hook_results.reviewers.split(',')
1536
1537 if self.GetIssue():
1538 latest_patchset = self.GetMostRecentPatchset()
1539 local_patchset = self.GetPatchset()
1540 if (latest_patchset and local_patchset and
1541 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001542 print('The last upload made from this repository was patchset #%d but '
1543 'the most recent patchset on the server is #%d.'
1544 % (local_patchset, latest_patchset))
1545 print('Uploading will still work, but if you\'ve uploaded to this '
1546 'issue from another machine or branch the patch you\'re '
1547 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 ask_for_data('About to upload; enter to confirm.')
1549
1550 print_stats(options.similarity, options.find_copies, git_diff_args)
1551 ret = self.CMDUploadChange(options, git_diff_args, change)
1552 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001553 if options.use_commit_queue:
1554 self.SetCQState(_CQState.COMMIT)
1555 elif options.cq_dry_run:
1556 self.SetCQState(_CQState.DRY_RUN)
1557
tandrii5d48c322016-08-18 16:19:37 -07001558 _git_set_branch_config_value('last-upload-hash',
1559 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 # Run post upload hooks, if specified.
1561 if settings.GetRunPostUploadHook():
1562 presubmit_support.DoPostUploadExecuter(
1563 change,
1564 self,
1565 settings.GetRoot(),
1566 options.verbose,
1567 sys.stdout)
1568
1569 # Upload all dependencies if specified.
1570 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001571 print()
1572 print('--dependencies has been specified.')
1573 print('All dependent local branches will be re-uploaded.')
1574 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 # Remove the dependencies flag from args so that we do not end up in a
1576 # loop.
1577 orig_args.remove('--dependencies')
1578 ret = upload_branch_deps(self, orig_args)
1579 return ret
1580
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001581 def SetCQState(self, new_state):
1582 """Update the CQ state for latest patchset.
1583
1584 Issue must have been already uploaded and known.
1585 """
1586 assert new_state in _CQState.ALL_STATES
1587 assert self.GetIssue()
1588 return self._codereview_impl.SetCQState(new_state)
1589
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 # Forward methods to codereview specific implementation.
1591
1592 def CloseIssue(self):
1593 return self._codereview_impl.CloseIssue()
1594
1595 def GetStatus(self):
1596 return self._codereview_impl.GetStatus()
1597
1598 def GetCodereviewServer(self):
1599 return self._codereview_impl.GetCodereviewServer()
1600
tandriide281ae2016-10-12 06:02:30 -07001601 def GetIssueOwner(self):
1602 """Get owner from codereview, which may differ from this checkout."""
1603 return self._codereview_impl.GetIssueOwner()
1604
1605 def GetIssueProject(self):
1606 """Get project from codereview, which may differ from what this
1607 checkout's codereview.settings or gerrit project URL say.
1608 """
1609 return self._codereview_impl.GetIssueProject()
1610
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001611 def GetApprovingReviewers(self):
1612 return self._codereview_impl.GetApprovingReviewers()
1613
1614 def GetMostRecentPatchset(self):
1615 return self._codereview_impl.GetMostRecentPatchset()
1616
tandriide281ae2016-10-12 06:02:30 -07001617 def CannotTriggerTryJobReason(self):
1618 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1619 return self._codereview_impl.CannotTriggerTryJobReason()
1620
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001621 def __getattr__(self, attr):
1622 # This is because lots of untested code accesses Rietveld-specific stuff
1623 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001624 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001625 # Note that child method defines __getattr__ as well, and forwards it here,
1626 # because _RietveldChangelistImpl is not cleaned up yet, and given
1627 # deprecation of Rietveld, it should probably be just removed.
1628 # Until that time, avoid infinite recursion by bypassing __getattr__
1629 # of implementation class.
1630 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001631
1632
1633class _ChangelistCodereviewBase(object):
1634 """Abstract base class encapsulating codereview specifics of a changelist."""
1635 def __init__(self, changelist):
1636 self._changelist = changelist # instance of Changelist
1637
1638 def __getattr__(self, attr):
1639 # Forward methods to changelist.
1640 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1641 # _RietveldChangelistImpl to avoid this hack?
1642 return getattr(self._changelist, attr)
1643
1644 def GetStatus(self):
1645 """Apply a rough heuristic to give a simple summary of an issue's review
1646 or CQ status, assuming adherence to a common workflow.
1647
1648 Returns None if no issue for this branch, or specific string keywords.
1649 """
1650 raise NotImplementedError()
1651
1652 def GetCodereviewServer(self):
1653 """Returns server URL without end slash, like "https://codereview.com"."""
1654 raise NotImplementedError()
1655
1656 def FetchDescription(self):
1657 """Fetches and returns description from the codereview server."""
1658 raise NotImplementedError()
1659
tandrii5d48c322016-08-18 16:19:37 -07001660 @classmethod
1661 def IssueConfigKey(cls):
1662 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001663 raise NotImplementedError()
1664
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001665 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001666 def PatchsetConfigKey(cls):
1667 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668 raise NotImplementedError()
1669
tandrii5d48c322016-08-18 16:19:37 -07001670 @classmethod
1671 def CodereviewServerConfigKey(cls):
1672 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 raise NotImplementedError()
1674
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001675 def _PostUnsetIssueProperties(self):
1676 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001677 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001678
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001679 def GetRieveldObjForPresubmit(self):
1680 # This is an unfortunate Rietveld-embeddedness in presubmit.
1681 # For non-Rietveld codereviews, this probably should return a dummy object.
1682 raise NotImplementedError()
1683
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001684 def GetGerritObjForPresubmit(self):
1685 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1686 return None
1687
dsansomee2d6fd92016-09-08 00:10:47 -07001688 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 """Update the description on codereview site."""
1690 raise NotImplementedError()
1691
1692 def CloseIssue(self):
1693 """Closes the issue."""
1694 raise NotImplementedError()
1695
1696 def GetApprovingReviewers(self):
1697 """Returns a list of reviewers approving the change.
1698
1699 Note: not necessarily committers.
1700 """
1701 raise NotImplementedError()
1702
1703 def GetMostRecentPatchset(self):
1704 """Returns the most recent patchset number from the codereview site."""
1705 raise NotImplementedError()
1706
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001707 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1708 directory):
1709 """Fetches and applies the issue.
1710
1711 Arguments:
1712 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1713 reject: if True, reject the failed patch instead of switching to 3-way
1714 merge. Rietveld only.
1715 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1716 only.
1717 directory: switch to directory before applying the patch. Rietveld only.
1718 """
1719 raise NotImplementedError()
1720
1721 @staticmethod
1722 def ParseIssueURL(parsed_url):
1723 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1724 failed."""
1725 raise NotImplementedError()
1726
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001727 def EnsureAuthenticated(self, force):
1728 """Best effort check that user is authenticated with codereview server.
1729
1730 Arguments:
1731 force: whether to skip confirmation questions.
1732 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001733 raise NotImplementedError()
1734
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001735 def CMDUploadChange(self, options, args, change):
1736 """Uploads a change to codereview."""
1737 raise NotImplementedError()
1738
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001739 def SetCQState(self, new_state):
1740 """Update the CQ state for latest patchset.
1741
1742 Issue must have been already uploaded and known.
1743 """
1744 raise NotImplementedError()
1745
tandriie113dfd2016-10-11 10:20:12 -07001746 def CannotTriggerTryJobReason(self):
1747 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1748 raise NotImplementedError()
1749
tandriide281ae2016-10-12 06:02:30 -07001750 def GetIssueOwner(self):
1751 raise NotImplementedError()
1752
1753 def GetIssueProject(self):
1754 raise NotImplementedError()
1755
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756
1757class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1758 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1759 super(_RietveldChangelistImpl, self).__init__(changelist)
1760 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001761 if not rietveld_server:
1762 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763
1764 self._rietveld_server = rietveld_server
1765 self._auth_config = auth_config
1766 self._props = None
1767 self._rpc_server = None
1768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetCodereviewServer(self):
1770 if not self._rietveld_server:
1771 # If we're on a branch then get the server potentially associated
1772 # with that branch.
1773 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001774 self._rietveld_server = gclient_utils.UpgradeToHttps(
1775 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 if not self._rietveld_server:
1777 self._rietveld_server = settings.GetDefaultServerUrl()
1778 return self._rietveld_server
1779
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001780 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001781 """Best effort check that user is authenticated with Rietveld server."""
1782 if self._auth_config.use_oauth2:
1783 authenticator = auth.get_authenticator_for_host(
1784 self.GetCodereviewServer(), self._auth_config)
1785 if not authenticator.has_cached_credentials():
1786 raise auth.LoginRequiredError(self.GetCodereviewServer())
1787
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 def FetchDescription(self):
1789 issue = self.GetIssue()
1790 assert issue
1791 try:
1792 return self.RpcServer().get_description(issue).strip()
1793 except urllib2.HTTPError as e:
1794 if e.code == 404:
1795 DieWithError(
1796 ('\nWhile fetching the description for issue %d, received a '
1797 '404 (not found)\n'
1798 'error. It is likely that you deleted this '
1799 'issue on the server. If this is the\n'
1800 'case, please run\n\n'
1801 ' git cl issue 0\n\n'
1802 'to clear the association with the deleted issue. Then run '
1803 'this command again.') % issue)
1804 else:
1805 DieWithError(
1806 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1807 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001808 print('Warning: Failed to retrieve CL description due to network '
1809 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 return ''
1811
1812 def GetMostRecentPatchset(self):
1813 return self.GetIssueProperties()['patchsets'][-1]
1814
1815 def GetPatchSetDiff(self, issue, patchset):
1816 return self.RpcServer().get(
1817 '/download/issue%s_%s.diff' % (issue, patchset))
1818
1819 def GetIssueProperties(self):
1820 if self._props is None:
1821 issue = self.GetIssue()
1822 if not issue:
1823 self._props = {}
1824 else:
1825 self._props = self.RpcServer().get_issue_properties(issue, True)
1826 return self._props
1827
tandriie113dfd2016-10-11 10:20:12 -07001828 def CannotTriggerTryJobReason(self):
1829 props = self.GetIssueProperties()
1830 if not props:
1831 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1832 if props.get('closed'):
1833 return 'CL %s is closed' % self.GetIssue()
1834 if props.get('private'):
1835 return 'CL %s is private' % self.GetIssue()
1836 return None
1837
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838 def GetApprovingReviewers(self):
1839 return get_approving_reviewers(self.GetIssueProperties())
1840
tandriide281ae2016-10-12 06:02:30 -07001841 def GetIssueOwner(self):
1842 return (self.GetIssueProperties() or {}).get('owner_email')
1843
1844 def GetIssueProject(self):
1845 return (self.GetIssueProperties() or {}).get('project')
1846
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001847 def AddComment(self, message):
1848 return self.RpcServer().add_comment(self.GetIssue(), message)
1849
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001850 def GetStatus(self):
1851 """Apply a rough heuristic to give a simple summary of an issue's review
1852 or CQ status, assuming adherence to a common workflow.
1853
1854 Returns None if no issue for this branch, or one of the following keywords:
1855 * 'error' - error from review tool (including deleted issues)
1856 * 'unsent' - not sent for review
1857 * 'waiting' - waiting for review
1858 * 'reply' - waiting for owner to reply to review
1859 * 'lgtm' - LGTM from at least one approved reviewer
1860 * 'commit' - in the commit queue
1861 * 'closed' - closed
1862 """
1863 if not self.GetIssue():
1864 return None
1865
1866 try:
1867 props = self.GetIssueProperties()
1868 except urllib2.HTTPError:
1869 return 'error'
1870
1871 if props.get('closed'):
1872 # Issue is closed.
1873 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001874 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001875 # Issue is in the commit queue.
1876 return 'commit'
1877
1878 try:
1879 reviewers = self.GetApprovingReviewers()
1880 except urllib2.HTTPError:
1881 return 'error'
1882
1883 if reviewers:
1884 # Was LGTM'ed.
1885 return 'lgtm'
1886
1887 messages = props.get('messages') or []
1888
tandrii9d2c7a32016-06-22 03:42:45 -07001889 # Skip CQ messages that don't require owner's action.
1890 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1891 if 'Dry run:' in messages[-1]['text']:
1892 messages.pop()
1893 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1894 # This message always follows prior messages from CQ,
1895 # so skip this too.
1896 messages.pop()
1897 else:
1898 # This is probably a CQ messages warranting user attention.
1899 break
1900
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001901 if not messages:
1902 # No message was sent.
1903 return 'unsent'
1904 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001905 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001906 return 'reply'
1907 return 'waiting'
1908
dsansomee2d6fd92016-09-08 00:10:47 -07001909 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001910 return self.RpcServer().update_description(
1911 self.GetIssue(), self.description)
1912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001914 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001916 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001917 return self.SetFlags({flag: value})
1918
1919 def SetFlags(self, flags):
1920 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001921 """
phajdan.jr68598232016-08-10 03:28:28 -07001922 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001923 try:
tandrii4b233bd2016-07-06 03:50:29 -07001924 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001925 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001926 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001927 if e.code == 404:
1928 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1929 if e.code == 403:
1930 DieWithError(
1931 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001932 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001933 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001935 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 """Returns an upload.RpcServer() to access this review's rietveld instance.
1937 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001938 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001939 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001940 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001941 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001942 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001944 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001945 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001946 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947
tandrii5d48c322016-08-18 16:19:37 -07001948 @classmethod
1949 def PatchsetConfigKey(cls):
1950 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951
tandrii5d48c322016-08-18 16:19:37 -07001952 @classmethod
1953 def CodereviewServerConfigKey(cls):
1954 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001955
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 def GetRieveldObjForPresubmit(self):
1957 return self.RpcServer()
1958
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001959 def SetCQState(self, new_state):
1960 props = self.GetIssueProperties()
1961 if props.get('private'):
1962 DieWithError('Cannot set-commit on private issue')
1963
1964 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001965 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001966 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001967 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001968 else:
tandrii4b233bd2016-07-06 03:50:29 -07001969 assert new_state == _CQState.DRY_RUN
1970 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001971
1972
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001973 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1974 directory):
1975 # TODO(maruel): Use apply_issue.py
1976
1977 # PatchIssue should never be called with a dirty tree. It is up to the
1978 # caller to check this, but just in case we assert here since the
1979 # consequences of the caller not checking this could be dire.
1980 assert(not git_common.is_dirty_git_tree('apply'))
1981 assert(parsed_issue_arg.valid)
1982 self._changelist.issue = parsed_issue_arg.issue
1983 if parsed_issue_arg.hostname:
1984 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1985
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001986 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1987 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001988 assert parsed_issue_arg.patchset
1989 patchset = parsed_issue_arg.patchset
1990 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1991 else:
1992 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1993 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1994
1995 # Switch up to the top-level directory, if necessary, in preparation for
1996 # applying the patch.
1997 top = settings.GetRelativeRoot()
1998 if top:
1999 os.chdir(top)
2000
2001 # Git patches have a/ at the beginning of source paths. We strip that out
2002 # with a sed script rather than the -p flag to patch so we can feed either
2003 # Git or svn-style patches into the same apply command.
2004 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
2005 try:
2006 patch_data = subprocess2.check_output(
2007 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2008 except subprocess2.CalledProcessError:
2009 DieWithError('Git patch mungling failed.')
2010 logging.info(patch_data)
2011
2012 # We use "git apply" to apply the patch instead of "patch" so that we can
2013 # pick up file adds.
2014 # The --index flag means: also insert into the index (so we catch adds).
2015 cmd = ['git', 'apply', '--index', '-p0']
2016 if directory:
2017 cmd.extend(('--directory', directory))
2018 if reject:
2019 cmd.append('--reject')
2020 elif IsGitVersionAtLeast('1.7.12'):
2021 cmd.append('--3way')
2022 try:
2023 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
2024 stdin=patch_data, stdout=subprocess2.VOID)
2025 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07002026 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002027 return 1
2028
2029 # If we had an issue, commit the current state and register the issue.
2030 if not nocommit:
2031 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2032 'patch from issue %(i)s at patchset '
2033 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2034 % {'i': self.GetIssue(), 'p': patchset})])
2035 self.SetIssue(self.GetIssue())
2036 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002037 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002038 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002039 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002040 return 0
2041
2042 @staticmethod
2043 def ParseIssueURL(parsed_url):
2044 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2045 return None
wychen3c1c1722016-08-04 11:46:36 -07002046 # Rietveld patch: https://domain/<number>/#ps<patchset>
2047 match = re.match(r'/(\d+)/$', parsed_url.path)
2048 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2049 if match and match2:
2050 return _RietveldParsedIssueNumberArgument(
2051 issue=int(match.group(1)),
2052 patchset=int(match2.group(1)),
2053 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002054 # Typical url: https://domain/<issue_number>[/[other]]
2055 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2056 if match:
2057 return _RietveldParsedIssueNumberArgument(
2058 issue=int(match.group(1)),
2059 hostname=parsed_url.netloc)
2060 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2061 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2062 if match:
2063 return _RietveldParsedIssueNumberArgument(
2064 issue=int(match.group(1)),
2065 patchset=int(match.group(2)),
2066 hostname=parsed_url.netloc,
2067 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
2068 return None
2069
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002070 def CMDUploadChange(self, options, args, change):
2071 """Upload the patch to Rietveld."""
2072 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2073 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002074 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2075 if options.emulate_svn_auto_props:
2076 upload_args.append('--emulate_svn_auto_props')
2077
2078 change_desc = None
2079
2080 if options.email is not None:
2081 upload_args.extend(['--email', options.email])
2082
2083 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002084 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002085 upload_args.extend(['--title', options.title])
2086 if options.message:
2087 upload_args.extend(['--message', options.message])
2088 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002089 print('This branch is associated with issue %s. '
2090 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002091 else:
nodirca166002016-06-27 10:59:51 -07002092 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002093 upload_args.extend(['--title', options.title])
2094 message = (options.title or options.message or
2095 CreateDescriptionFromLog(args))
2096 change_desc = ChangeDescription(message)
2097 if options.reviewers or options.tbr_owners:
2098 change_desc.update_reviewers(options.reviewers,
2099 options.tbr_owners,
2100 change)
2101 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002102 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002103
2104 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002105 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002106 return 1
2107
2108 upload_args.extend(['--message', change_desc.description])
2109 if change_desc.get_reviewers():
2110 upload_args.append('--reviewers=%s' % ','.join(
2111 change_desc.get_reviewers()))
2112 if options.send_mail:
2113 if not change_desc.get_reviewers():
2114 DieWithError("Must specify reviewers to send email.")
2115 upload_args.append('--send_mail')
2116
2117 # We check this before applying rietveld.private assuming that in
2118 # rietveld.cc only addresses which we can send private CLs to are listed
2119 # if rietveld.private is set, and so we should ignore rietveld.cc only
2120 # when --private is specified explicitly on the command line.
2121 if options.private:
2122 logging.warn('rietveld.cc is ignored since private flag is specified. '
2123 'You need to review and add them manually if necessary.')
2124 cc = self.GetCCListWithoutDefault()
2125 else:
2126 cc = self.GetCCList()
2127 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2128 if cc:
2129 upload_args.extend(['--cc', cc])
2130
2131 if options.private or settings.GetDefaultPrivateFlag() == "True":
2132 upload_args.append('--private')
2133
2134 upload_args.extend(['--git_similarity', str(options.similarity)])
2135 if not options.find_copies:
2136 upload_args.extend(['--git_no_find_copies'])
2137
2138 # Include the upstream repo's URL in the change -- this is useful for
2139 # projects that have their source spread across multiple repos.
2140 remote_url = self.GetGitBaseUrlFromConfig()
2141 if not remote_url:
2142 if settings.GetIsGitSvn():
2143 remote_url = self.GetGitSvnRemoteUrl()
2144 else:
2145 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2146 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2147 self.GetUpstreamBranch().split('/')[-1])
2148 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 remote, remote_branch = self.GetRemoteBranch()
2150 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2151 settings.GetPendingRefPrefix())
2152 if target_ref:
2153 upload_args.extend(['--target_ref', target_ref])
2154
2155 # Look for dependent patchsets. See crbug.com/480453 for more details.
2156 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2157 upstream_branch = ShortBranchName(upstream_branch)
2158 if remote is '.':
2159 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002160 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002161 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print()
2163 print('Skipping dependency patchset upload because git config '
2164 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2165 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 else:
2167 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002168 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002169 auth_config=auth_config)
2170 branch_cl_issue_url = branch_cl.GetIssueURL()
2171 branch_cl_issue = branch_cl.GetIssue()
2172 branch_cl_patchset = branch_cl.GetPatchset()
2173 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2174 upload_args.extend(
2175 ['--depends_on_patchset', '%s:%s' % (
2176 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002177 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 '\n'
2179 'The current branch (%s) is tracking a local branch (%s) with '
2180 'an associated CL.\n'
2181 'Adding %s/#ps%s as a dependency patchset.\n'
2182 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2183 branch_cl_patchset))
2184
2185 project = settings.GetProject()
2186 if project:
2187 upload_args.extend(['--project', project])
2188
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189 try:
2190 upload_args = ['upload'] + upload_args + args
2191 logging.info('upload.RealMain(%s)', upload_args)
2192 issue, patchset = upload.RealMain(upload_args)
2193 issue = int(issue)
2194 patchset = int(patchset)
2195 except KeyboardInterrupt:
2196 sys.exit(1)
2197 except:
2198 # If we got an exception after the user typed a description for their
2199 # change, back up the description before re-raising.
2200 if change_desc:
2201 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2202 print('\nGot exception while uploading -- saving description to %s\n' %
2203 backup_path)
2204 backup_file = open(backup_path, 'w')
2205 backup_file.write(change_desc.description)
2206 backup_file.close()
2207 raise
2208
2209 if not self.GetIssue():
2210 self.SetIssue(issue)
2211 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002212 return 0
2213
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214
2215class _GerritChangelistImpl(_ChangelistCodereviewBase):
2216 def __init__(self, changelist, auth_config=None):
2217 # auth_config is Rietveld thing, kept here to preserve interface only.
2218 super(_GerritChangelistImpl, self).__init__(changelist)
2219 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002220 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002221 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002222 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002223
2224 def _GetGerritHost(self):
2225 # Lazy load of configs.
2226 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002227 if self._gerrit_host and '.' not in self._gerrit_host:
2228 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2229 # This happens for internal stuff http://crbug.com/614312.
2230 parsed = urlparse.urlparse(self.GetRemoteUrl())
2231 if parsed.scheme == 'sso':
2232 print('WARNING: using non https URLs for remote is likely broken\n'
2233 ' Your current remote is: %s' % self.GetRemoteUrl())
2234 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2235 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002236 return self._gerrit_host
2237
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002238 def _GetGitHost(self):
2239 """Returns git host to be used when uploading change to Gerrit."""
2240 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2241
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002242 def GetCodereviewServer(self):
2243 if not self._gerrit_server:
2244 # If we're on a branch then get the server potentially associated
2245 # with that branch.
2246 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002247 self._gerrit_server = self._GitGetBranchConfigValue(
2248 self.CodereviewServerConfigKey())
2249 if self._gerrit_server:
2250 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002251 if not self._gerrit_server:
2252 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2253 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002254 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002255 parts[0] = parts[0] + '-review'
2256 self._gerrit_host = '.'.join(parts)
2257 self._gerrit_server = 'https://%s' % self._gerrit_host
2258 return self._gerrit_server
2259
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002260 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002261 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002262 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002263
tandrii5d48c322016-08-18 16:19:37 -07002264 @classmethod
2265 def PatchsetConfigKey(cls):
2266 return 'gerritpatchset'
2267
2268 @classmethod
2269 def CodereviewServerConfigKey(cls):
2270 return 'gerritserver'
2271
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002272 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002273 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002274 if settings.GetGerritSkipEnsureAuthenticated():
2275 # For projects with unusual authentication schemes.
2276 # See http://crbug.com/603378.
2277 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002278 # Lazy-loader to identify Gerrit and Git hosts.
2279 if gerrit_util.GceAuthenticator.is_gce():
2280 return
2281 self.GetCodereviewServer()
2282 git_host = self._GetGitHost()
2283 assert self._gerrit_server and self._gerrit_host
2284 cookie_auth = gerrit_util.CookiesAuthenticator()
2285
2286 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2287 git_auth = cookie_auth.get_auth_header(git_host)
2288 if gerrit_auth and git_auth:
2289 if gerrit_auth == git_auth:
2290 return
2291 print((
2292 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2293 ' Check your %s or %s file for credentials of hosts:\n'
2294 ' %s\n'
2295 ' %s\n'
2296 ' %s') %
2297 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2298 git_host, self._gerrit_host,
2299 cookie_auth.get_new_password_message(git_host)))
2300 if not force:
2301 ask_for_data('If you know what you are doing, press Enter to continue, '
2302 'Ctrl+C to abort.')
2303 return
2304 else:
2305 missing = (
2306 [] if gerrit_auth else [self._gerrit_host] +
2307 [] if git_auth else [git_host])
2308 DieWithError('Credentials for the following hosts are required:\n'
2309 ' %s\n'
2310 'These are read from %s (or legacy %s)\n'
2311 '%s' % (
2312 '\n '.join(missing),
2313 cookie_auth.get_gitcookies_path(),
2314 cookie_auth.get_netrc_path(),
2315 cookie_auth.get_new_password_message(git_host)))
2316
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002317 def _PostUnsetIssueProperties(self):
2318 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002319 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002320
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321 def GetRieveldObjForPresubmit(self):
2322 class ThisIsNotRietveldIssue(object):
2323 def __nonzero__(self):
2324 # This is a hack to make presubmit_support think that rietveld is not
2325 # defined, yet still ensure that calls directly result in a decent
2326 # exception message below.
2327 return False
2328
2329 def __getattr__(self, attr):
2330 print(
2331 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2332 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2333 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2334 'or use Rietveld for codereview.\n'
2335 'See also http://crbug.com/579160.' % attr)
2336 raise NotImplementedError()
2337 return ThisIsNotRietveldIssue()
2338
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002339 def GetGerritObjForPresubmit(self):
2340 return presubmit_support.GerritAccessor(self._GetGerritHost())
2341
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002342 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002343 """Apply a rough heuristic to give a simple summary of an issue's review
2344 or CQ status, assuming adherence to a common workflow.
2345
2346 Returns None if no issue for this branch, or one of the following keywords:
2347 * 'error' - error from review tool (including deleted issues)
2348 * 'unsent' - no reviewers added
2349 * 'waiting' - waiting for review
2350 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002351 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2352 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002353 * 'commit' - in the commit queue
2354 * 'closed' - abandoned
2355 """
2356 if not self.GetIssue():
2357 return None
2358
2359 try:
2360 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002361 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002362 return 'error'
2363
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002364 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002365 return 'closed'
2366
2367 cq_label = data['labels'].get('Commit-Queue', {})
2368 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002369 votes = cq_label.get('all', [])
2370 highest_vote = 0
2371 for v in votes:
2372 highest_vote = max(highest_vote, v.get('value', 0))
2373 vote_value = str(highest_vote)
2374 if vote_value != '0':
2375 # Add a '+' if the value is not 0 to match the values in the label.
2376 # The cq_label does not have negatives.
2377 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002378 vote_text = cq_label.get('values', {}).get(vote_value, '')
2379 if vote_text.lower() == 'commit':
2380 return 'commit'
2381
2382 lgtm_label = data['labels'].get('Code-Review', {})
2383 if lgtm_label:
2384 if 'rejected' in lgtm_label:
2385 return 'not lgtm'
2386 if 'approved' in lgtm_label:
2387 return 'lgtm'
2388
2389 if not data.get('reviewers', {}).get('REVIEWER', []):
2390 return 'unsent'
2391
2392 messages = data.get('messages', [])
2393 if messages:
2394 owner = data['owner'].get('_account_id')
2395 last_message_author = messages[-1].get('author', {}).get('_account_id')
2396 if owner != last_message_author:
2397 # Some reply from non-owner.
2398 return 'reply'
2399
2400 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401
2402 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002403 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404 return data['revisions'][data['current_revision']]['_number']
2405
2406 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002407 data = self._GetChangeDetail(['CURRENT_REVISION'])
2408 current_rev = data['current_revision']
2409 url = data['revisions'][current_rev]['fetch']['http']['url']
2410 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002411
dsansomee2d6fd92016-09-08 00:10:47 -07002412 def UpdateDescriptionRemote(self, description, force=False):
2413 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2414 if not force:
2415 ask_for_data(
2416 'The description cannot be modified while the issue has a pending '
2417 'unpublished edit. Either publish the edit in the Gerrit web UI '
2418 'or delete it.\n\n'
2419 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2420
2421 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2422 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002423 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2424 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002425
2426 def CloseIssue(self):
2427 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2428
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002429 def GetApprovingReviewers(self):
2430 """Returns a list of reviewers approving the change.
2431
2432 Note: not necessarily committers.
2433 """
2434 raise NotImplementedError()
2435
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002436 def SubmitIssue(self, wait_for_merge=True):
2437 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2438 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002440 def _GetChangeDetail(self, options=None, issue=None):
2441 options = options or []
2442 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002443 assert issue, 'issue is required to query Gerrit'
2444 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002445 options)
tandriic2405f52016-10-10 08:13:15 -07002446 if not data:
2447 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2448 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002449
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002450 def CMDLand(self, force, bypass_hooks, verbose):
2451 if git_common.is_dirty_git_tree('land'):
2452 return 1
tandriid60367b2016-06-22 05:25:12 -07002453 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2454 if u'Commit-Queue' in detail.get('labels', {}):
2455 if not force:
2456 ask_for_data('\nIt seems this repository has a Commit Queue, '
2457 'which can test and land changes for you. '
2458 'Are you sure you wish to bypass it?\n'
2459 'Press Enter to continue, Ctrl+C to abort.')
2460
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002461 differs = True
tandriic4344b52016-08-29 06:04:54 -07002462 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002463 # Note: git diff outputs nothing if there is no diff.
2464 if not last_upload or RunGit(['diff', last_upload]).strip():
2465 print('WARNING: some changes from local branch haven\'t been uploaded')
2466 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002467 if detail['current_revision'] == last_upload:
2468 differs = False
2469 else:
2470 print('WARNING: local branch contents differ from latest uploaded '
2471 'patchset')
2472 if differs:
2473 if not force:
2474 ask_for_data(
2475 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2476 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2477 elif not bypass_hooks:
2478 hook_results = self.RunHook(
2479 committing=True,
2480 may_prompt=not force,
2481 verbose=verbose,
2482 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2483 if not hook_results.should_continue():
2484 return 1
2485
2486 self.SubmitIssue(wait_for_merge=True)
2487 print('Issue %s has been submitted.' % self.GetIssueURL())
2488 return 0
2489
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002490 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2491 directory):
2492 assert not reject
2493 assert not nocommit
2494 assert not directory
2495 assert parsed_issue_arg.valid
2496
2497 self._changelist.issue = parsed_issue_arg.issue
2498
2499 if parsed_issue_arg.hostname:
2500 self._gerrit_host = parsed_issue_arg.hostname
2501 self._gerrit_server = 'https://%s' % self._gerrit_host
2502
tandriic2405f52016-10-10 08:13:15 -07002503 try:
2504 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2505 except GerritIssueNotExists as e:
2506 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002507
2508 if not parsed_issue_arg.patchset:
2509 # Use current revision by default.
2510 revision_info = detail['revisions'][detail['current_revision']]
2511 patchset = int(revision_info['_number'])
2512 else:
2513 patchset = parsed_issue_arg.patchset
2514 for revision_info in detail['revisions'].itervalues():
2515 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2516 break
2517 else:
2518 DieWithError('Couldn\'t find patchset %i in issue %i' %
2519 (parsed_issue_arg.patchset, self.GetIssue()))
2520
2521 fetch_info = revision_info['fetch']['http']
2522 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2523 RunGit(['cherry-pick', 'FETCH_HEAD'])
2524 self.SetIssue(self.GetIssue())
2525 self.SetPatchset(patchset)
2526 print('Committed patch for issue %i pathset %i locally' %
2527 (self.GetIssue(), self.GetPatchset()))
2528 return 0
2529
2530 @staticmethod
2531 def ParseIssueURL(parsed_url):
2532 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2533 return None
2534 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2535 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2536 # Short urls like https://domain/<issue_number> can be used, but don't allow
2537 # specifying the patchset (you'd 404), but we allow that here.
2538 if parsed_url.path == '/':
2539 part = parsed_url.fragment
2540 else:
2541 part = parsed_url.path
2542 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2543 if match:
2544 return _ParsedIssueNumberArgument(
2545 issue=int(match.group(2)),
2546 patchset=int(match.group(4)) if match.group(4) else None,
2547 hostname=parsed_url.netloc)
2548 return None
2549
tandrii16e0b4e2016-06-07 10:34:28 -07002550 def _GerritCommitMsgHookCheck(self, offer_removal):
2551 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2552 if not os.path.exists(hook):
2553 return
2554 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2555 # custom developer made one.
2556 data = gclient_utils.FileRead(hook)
2557 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2558 return
2559 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002560 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002561 'and may interfere with it in subtle ways.\n'
2562 'We recommend you remove the commit-msg hook.')
2563 if offer_removal:
2564 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2565 if reply.lower().startswith('y'):
2566 gclient_utils.rm_file_or_tree(hook)
2567 print('Gerrit commit-msg hook removed.')
2568 else:
2569 print('OK, will keep Gerrit commit-msg hook in place.')
2570
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002571 def CMDUploadChange(self, options, args, change):
2572 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002573 if options.squash and options.no_squash:
2574 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002575
2576 if not options.squash and not options.no_squash:
2577 # Load default for user, repo, squash=true, in this order.
2578 options.squash = settings.GetSquashGerritUploads()
2579 elif options.no_squash:
2580 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002581
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002582 # We assume the remote called "origin" is the one we want.
2583 # It is probably not worthwhile to support different workflows.
2584 gerrit_remote = 'origin'
2585
2586 remote, remote_branch = self.GetRemoteBranch()
2587 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2588 pending_prefix='')
2589
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002590 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002591 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002592 if self.GetIssue():
2593 # Try to get the message from a previous upload.
2594 message = self.GetDescription()
2595 if not message:
2596 DieWithError(
2597 'failed to fetch description from current Gerrit issue %d\n'
2598 '%s' % (self.GetIssue(), self.GetIssueURL()))
2599 change_id = self._GetChangeDetail()['change_id']
2600 while True:
2601 footer_change_ids = git_footers.get_footer_change_id(message)
2602 if footer_change_ids == [change_id]:
2603 break
2604 if not footer_change_ids:
2605 message = git_footers.add_footer_change_id(message, change_id)
2606 print('WARNING: appended missing Change-Id to issue description')
2607 continue
2608 # There is already a valid footer but with different or several ids.
2609 # Doing this automatically is non-trivial as we don't want to lose
2610 # existing other footers, yet we want to append just 1 desired
2611 # Change-Id. Thus, just create a new footer, but let user verify the
2612 # new description.
2613 message = '%s\n\nChange-Id: %s' % (message, change_id)
2614 print(
2615 'WARNING: issue %s has Change-Id footer(s):\n'
2616 ' %s\n'
2617 'but issue has Change-Id %s, according to Gerrit.\n'
2618 'Please, check the proposed correction to the description, '
2619 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2620 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2621 change_id))
2622 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2623 if not options.force:
2624 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002625 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002626 message = change_desc.description
2627 if not message:
2628 DieWithError("Description is empty. Aborting...")
2629 # Continue the while loop.
2630 # Sanity check of this code - we should end up with proper message
2631 # footer.
2632 assert [change_id] == git_footers.get_footer_change_id(message)
2633 change_desc = ChangeDescription(message)
2634 else:
2635 change_desc = ChangeDescription(
2636 options.message or CreateDescriptionFromLog(args))
2637 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002638 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002639 if not change_desc.description:
2640 DieWithError("Description is empty. Aborting...")
2641 message = change_desc.description
2642 change_ids = git_footers.get_footer_change_id(message)
2643 if len(change_ids) > 1:
2644 DieWithError('too many Change-Id footers, at most 1 allowed.')
2645 if not change_ids:
2646 # Generate the Change-Id automatically.
2647 message = git_footers.add_footer_change_id(
2648 message, GenerateGerritChangeId(message))
2649 change_desc.set_description(message)
2650 change_ids = git_footers.get_footer_change_id(message)
2651 assert len(change_ids) == 1
2652 change_id = change_ids[0]
2653
2654 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2655 if remote is '.':
2656 # If our upstream branch is local, we base our squashed commit on its
2657 # squashed version.
2658 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2659 # Check the squashed hash of the parent.
2660 parent = RunGit(['config',
2661 'branch.%s.gerritsquashhash' % upstream_branch_name],
2662 error_ok=True).strip()
2663 # Verify that the upstream branch has been uploaded too, otherwise
2664 # Gerrit will create additional CLs when uploading.
2665 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2666 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 DieWithError(
2668 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002669 'Note: maybe you\'ve uploaded it with --no-squash. '
2670 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002671 ' git cl upload --squash\n' % upstream_branch_name)
2672 else:
2673 parent = self.GetCommonAncestorWithUpstream()
2674
2675 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2676 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2677 '-m', message]).strip()
2678 else:
2679 change_desc = ChangeDescription(
2680 options.message or CreateDescriptionFromLog(args))
2681 if not change_desc.description:
2682 DieWithError("Description is empty. Aborting...")
2683
2684 if not git_footers.get_footer_change_id(change_desc.description):
2685 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002686 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2687 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002688 ref_to_push = 'HEAD'
2689 parent = '%s/%s' % (gerrit_remote, branch)
2690 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2691
2692 assert change_desc
2693 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2694 ref_to_push)]).splitlines()
2695 if len(commits) > 1:
2696 print('WARNING: This will upload %d commits. Run the following command '
2697 'to see which commits will be uploaded: ' % len(commits))
2698 print('git log %s..%s' % (parent, ref_to_push))
2699 print('You can also use `git squash-branch` to squash these into a '
2700 'single commit.')
2701 ask_for_data('About to upload; enter to confirm.')
2702
2703 if options.reviewers or options.tbr_owners:
2704 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2705 change)
2706
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002707 # Extra options that can be specified at push time. Doc:
2708 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2709 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002710 if change_desc.get_reviewers(tbr_only=True):
2711 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2712 refspec_opts.append('l=Code-Review+1')
2713
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002714 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002715 if not re.match(r'^[\w ]+$', options.title):
2716 options.title = re.sub(r'[^\w ]', '', options.title)
2717 print('WARNING: Patchset title may only contain alphanumeric chars '
2718 'and spaces. Cleaned up title:\n%s' % options.title)
2719 if not options.force:
2720 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002721 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2722 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002723 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2724
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002725 if options.send_mail:
2726 if not change_desc.get_reviewers():
2727 DieWithError('Must specify reviewers to send email.')
2728 refspec_opts.append('notify=ALL')
2729 else:
2730 refspec_opts.append('notify=NONE')
2731
tandrii99a72f22016-08-17 14:33:24 -07002732 reviewers = change_desc.get_reviewers()
2733 if reviewers:
2734 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002735
agablec6787972016-09-09 16:13:34 -07002736 if options.private:
2737 refspec_opts.append('draft')
2738
rmistry9eadede2016-09-19 11:22:43 -07002739 if options.topic:
2740 # Documentation on Gerrit topics is here:
2741 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2742 refspec_opts.append('topic=%s' % options.topic)
2743
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002744 refspec_suffix = ''
2745 if refspec_opts:
2746 refspec_suffix = '%' + ','.join(refspec_opts)
2747 assert ' ' not in refspec_suffix, (
2748 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002749 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002750
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002752 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002753 print_stdout=True,
2754 # Flush after every line: useful for seeing progress when running as
2755 # recipe.
2756 filter_fn=lambda _: sys.stdout.flush())
2757
2758 if options.squash:
2759 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2760 change_numbers = [m.group(1)
2761 for m in map(regex.match, push_stdout.splitlines())
2762 if m]
2763 if len(change_numbers) != 1:
2764 DieWithError(
2765 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2766 'Change-Id: %s') % (len(change_numbers), change_id))
2767 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002768 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002769
2770 # Add cc's from the CC_LIST and --cc flag (if any).
2771 cc = self.GetCCList().split(',')
2772 if options.cc:
2773 cc.extend(options.cc)
2774 cc = filter(None, [email.strip() for email in cc])
2775 if cc:
2776 gerrit_util.AddReviewers(
2777 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2778
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002779 return 0
2780
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002781 def _AddChangeIdToCommitMessage(self, options, args):
2782 """Re-commits using the current message, assumes the commit hook is in
2783 place.
2784 """
2785 log_desc = options.message or CreateDescriptionFromLog(args)
2786 git_command = ['commit', '--amend', '-m', log_desc]
2787 RunGit(git_command)
2788 new_log_desc = CreateDescriptionFromLog(args)
2789 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002790 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002791 return new_log_desc
2792 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002793 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002794
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002795 def SetCQState(self, new_state):
2796 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002797 vote_map = {
2798 _CQState.NONE: 0,
2799 _CQState.DRY_RUN: 1,
2800 _CQState.COMMIT : 2,
2801 }
2802 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2803 labels={'Commit-Queue': vote_map[new_state]})
2804
tandriie113dfd2016-10-11 10:20:12 -07002805 def CannotTriggerTryJobReason(self):
2806 # TODO(tandrii): implement for Gerrit.
2807 raise NotImplementedError()
2808
tandriide281ae2016-10-12 06:02:30 -07002809 def GetIssueOwner(self):
2810 # TODO(tandrii): implement for Gerrit.
2811 raise NotImplementedError()
2812
2813 def GetIssueProject(self):
2814 # TODO(tandrii): implement for Gerrit.
2815 raise NotImplementedError()
2816
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002817
2818_CODEREVIEW_IMPLEMENTATIONS = {
2819 'rietveld': _RietveldChangelistImpl,
2820 'gerrit': _GerritChangelistImpl,
2821}
2822
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002823
iannuccie53c9352016-08-17 14:40:40 -07002824def _add_codereview_issue_select_options(parser, extra=""):
2825 _add_codereview_select_options(parser)
2826
2827 text = ('Operate on this issue number instead of the current branch\'s '
2828 'implicit issue.')
2829 if extra:
2830 text += ' '+extra
2831 parser.add_option('-i', '--issue', type=int, help=text)
2832
2833
2834def _process_codereview_issue_select_options(parser, options):
2835 _process_codereview_select_options(parser, options)
2836 if options.issue is not None and not options.forced_codereview:
2837 parser.error('--issue must be specified with either --rietveld or --gerrit')
2838
2839
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002840def _add_codereview_select_options(parser):
2841 """Appends --gerrit and --rietveld options to force specific codereview."""
2842 parser.codereview_group = optparse.OptionGroup(
2843 parser, 'EXPERIMENTAL! Codereview override options')
2844 parser.add_option_group(parser.codereview_group)
2845 parser.codereview_group.add_option(
2846 '--gerrit', action='store_true',
2847 help='Force the use of Gerrit for codereview')
2848 parser.codereview_group.add_option(
2849 '--rietveld', action='store_true',
2850 help='Force the use of Rietveld for codereview')
2851
2852
2853def _process_codereview_select_options(parser, options):
2854 if options.gerrit and options.rietveld:
2855 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2856 options.forced_codereview = None
2857 if options.gerrit:
2858 options.forced_codereview = 'gerrit'
2859 elif options.rietveld:
2860 options.forced_codereview = 'rietveld'
2861
2862
tandriif9aefb72016-07-01 09:06:51 -07002863def _get_bug_line_values(default_project, bugs):
2864 """Given default_project and comma separated list of bugs, yields bug line
2865 values.
2866
2867 Each bug can be either:
2868 * a number, which is combined with default_project
2869 * string, which is left as is.
2870
2871 This function may produce more than one line, because bugdroid expects one
2872 project per line.
2873
2874 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2875 ['v8:123', 'chromium:789']
2876 """
2877 default_bugs = []
2878 others = []
2879 for bug in bugs.split(','):
2880 bug = bug.strip()
2881 if bug:
2882 try:
2883 default_bugs.append(int(bug))
2884 except ValueError:
2885 others.append(bug)
2886
2887 if default_bugs:
2888 default_bugs = ','.join(map(str, default_bugs))
2889 if default_project:
2890 yield '%s:%s' % (default_project, default_bugs)
2891 else:
2892 yield default_bugs
2893 for other in sorted(others):
2894 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2895 yield other
2896
2897
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002898class ChangeDescription(object):
2899 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002900 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002901 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002902
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002903 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002904 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002905
agable@chromium.org42c20792013-09-12 17:34:49 +00002906 @property # www.logilab.org/ticket/89786
2907 def description(self): # pylint: disable=E0202
2908 return '\n'.join(self._description_lines)
2909
2910 def set_description(self, desc):
2911 if isinstance(desc, basestring):
2912 lines = desc.splitlines()
2913 else:
2914 lines = [line.rstrip() for line in desc]
2915 while lines and not lines[0]:
2916 lines.pop(0)
2917 while lines and not lines[-1]:
2918 lines.pop(-1)
2919 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002920
piman@chromium.org336f9122014-09-04 02:16:55 +00002921 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002922 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002923 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002924 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002925 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002926 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002927
agable@chromium.org42c20792013-09-12 17:34:49 +00002928 # Get the set of R= and TBR= lines and remove them from the desciption.
2929 regexp = re.compile(self.R_LINE)
2930 matches = [regexp.match(line) for line in self._description_lines]
2931 new_desc = [l for i, l in enumerate(self._description_lines)
2932 if not matches[i]]
2933 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002934
agable@chromium.org42c20792013-09-12 17:34:49 +00002935 # Construct new unified R= and TBR= lines.
2936 r_names = []
2937 tbr_names = []
2938 for match in matches:
2939 if not match:
2940 continue
2941 people = cleanup_list([match.group(2).strip()])
2942 if match.group(1) == 'TBR':
2943 tbr_names.extend(people)
2944 else:
2945 r_names.extend(people)
2946 for name in r_names:
2947 if name not in reviewers:
2948 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002949 if add_owners_tbr:
2950 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002951 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002952 all_reviewers = set(tbr_names + reviewers)
2953 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2954 all_reviewers)
2955 tbr_names.extend(owners_db.reviewers_for(missing_files,
2956 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002957 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2958 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2959
2960 # Put the new lines in the description where the old first R= line was.
2961 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2962 if 0 <= line_loc < len(self._description_lines):
2963 if new_tbr_line:
2964 self._description_lines.insert(line_loc, new_tbr_line)
2965 if new_r_line:
2966 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002967 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002968 if new_r_line:
2969 self.append_footer(new_r_line)
2970 if new_tbr_line:
2971 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002972
tandriif9aefb72016-07-01 09:06:51 -07002973 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002974 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002975 self.set_description([
2976 '# Enter a description of the change.',
2977 '# This will be displayed on the codereview site.',
2978 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002979 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002980 '--------------------',
2981 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002982
agable@chromium.org42c20792013-09-12 17:34:49 +00002983 regexp = re.compile(self.BUG_LINE)
2984 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002985 prefix = settings.GetBugPrefix()
2986 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2987 for value in values:
2988 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2989 self.append_footer('BUG=%s' % value)
2990
agable@chromium.org42c20792013-09-12 17:34:49 +00002991 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002992 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002993 if not content:
2994 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002995 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002996
2997 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002998 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2999 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003000 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003002
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003003 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003004 """Adds a footer line to the description.
3005
3006 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3007 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3008 that Gerrit footers are always at the end.
3009 """
3010 parsed_footer_line = git_footers.parse_footer(line)
3011 if parsed_footer_line:
3012 # Line is a gerrit footer in the form: Footer-Key: any value.
3013 # Thus, must be appended observing Gerrit footer rules.
3014 self.set_description(
3015 git_footers.add_footer(self.description,
3016 key=parsed_footer_line[0],
3017 value=parsed_footer_line[1]))
3018 return
3019
3020 if not self._description_lines:
3021 self._description_lines.append(line)
3022 return
3023
3024 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3025 if gerrit_footers:
3026 # git_footers.split_footers ensures that there is an empty line before
3027 # actual (gerrit) footers, if any. We have to keep it that way.
3028 assert top_lines and top_lines[-1] == ''
3029 top_lines, separator = top_lines[:-1], top_lines[-1:]
3030 else:
3031 separator = [] # No need for separator if there are no gerrit_footers.
3032
3033 prev_line = top_lines[-1] if top_lines else ''
3034 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3035 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3036 top_lines.append('')
3037 top_lines.append(line)
3038 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003039
tandrii99a72f22016-08-17 14:33:24 -07003040 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003041 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003042 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003043 reviewers = [match.group(2).strip()
3044 for match in matches
3045 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003046 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003047
3048
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003049def get_approving_reviewers(props):
3050 """Retrieves the reviewers that approved a CL from the issue properties with
3051 messages.
3052
3053 Note that the list may contain reviewers that are not committer, thus are not
3054 considered by the CQ.
3055 """
3056 return sorted(
3057 set(
3058 message['sender']
3059 for message in props['messages']
3060 if message['approval'] and message['sender'] in props['reviewers']
3061 )
3062 )
3063
3064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065def FindCodereviewSettingsFile(filename='codereview.settings'):
3066 """Finds the given file starting in the cwd and going up.
3067
3068 Only looks up to the top of the repository unless an
3069 'inherit-review-settings-ok' file exists in the root of the repository.
3070 """
3071 inherit_ok_file = 'inherit-review-settings-ok'
3072 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003073 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3075 root = '/'
3076 while True:
3077 if filename in os.listdir(cwd):
3078 if os.path.isfile(os.path.join(cwd, filename)):
3079 return open(os.path.join(cwd, filename))
3080 if cwd == root:
3081 break
3082 cwd = os.path.dirname(cwd)
3083
3084
3085def LoadCodereviewSettingsFromFile(fileobj):
3086 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003087 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089 def SetProperty(name, setting, unset_error_ok=False):
3090 fullname = 'rietveld.' + name
3091 if setting in keyvals:
3092 RunGit(['config', fullname, keyvals[setting]])
3093 else:
3094 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3095
tandrii48df5812016-10-17 03:55:37 -07003096 if not keyvals.get('GERRIT_HOST', False):
3097 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098 # Only server setting is required. Other settings can be absent.
3099 # In that case, we ignore errors raised during option deletion attempt.
3100 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003101 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003102 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3103 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003104 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003105 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003106 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3107 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003108 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003109 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003110 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003111 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003112 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3113 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003115 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003116 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003117
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003118 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003119 RunGit(['config', 'gerrit.squash-uploads',
3120 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003121
tandrii@chromium.org28253532016-04-14 13:46:56 +00003122 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003123 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003124 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3127 #should be of the form
3128 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3129 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3130 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3131 keyvals['ORIGIN_URL_CONFIG']])
3132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003133
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003134def urlretrieve(source, destination):
3135 """urllib is broken for SSL connections via a proxy therefore we
3136 can't use urllib.urlretrieve()."""
3137 with open(destination, 'w') as f:
3138 f.write(urllib2.urlopen(source).read())
3139
3140
ukai@chromium.org712d6102013-11-27 00:52:58 +00003141def hasSheBang(fname):
3142 """Checks fname is a #! script."""
3143 with open(fname) as f:
3144 return f.read(2).startswith('#!')
3145
3146
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003147# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3148def DownloadHooks(*args, **kwargs):
3149 pass
3150
3151
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003152def DownloadGerritHook(force):
3153 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003154
3155 Args:
3156 force: True to update hooks. False to install hooks if not present.
3157 """
3158 if not settings.GetIsGerrit():
3159 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003160 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003161 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3162 if not os.access(dst, os.X_OK):
3163 if os.path.exists(dst):
3164 if not force:
3165 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003166 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003167 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003168 if not hasSheBang(dst):
3169 DieWithError('Not a script: %s\n'
3170 'You need to download from\n%s\n'
3171 'into .git/hooks/commit-msg and '
3172 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003173 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3174 except Exception:
3175 if os.path.exists(dst):
3176 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003177 DieWithError('\nFailed to download hooks.\n'
3178 'You need to download from\n%s\n'
3179 'into .git/hooks/commit-msg and '
3180 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003181
3182
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003183
3184def GetRietveldCodereviewSettingsInteractively():
3185 """Prompt the user for settings."""
3186 server = settings.GetDefaultServerUrl(error_ok=True)
3187 prompt = 'Rietveld server (host[:port])'
3188 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3189 newserver = ask_for_data(prompt + ':')
3190 if not server and not newserver:
3191 newserver = DEFAULT_SERVER
3192 if newserver:
3193 newserver = gclient_utils.UpgradeToHttps(newserver)
3194 if newserver != server:
3195 RunGit(['config', 'rietveld.server', newserver])
3196
3197 def SetProperty(initial, caption, name, is_url):
3198 prompt = caption
3199 if initial:
3200 prompt += ' ("x" to clear) [%s]' % initial
3201 new_val = ask_for_data(prompt + ':')
3202 if new_val == 'x':
3203 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3204 elif new_val:
3205 if is_url:
3206 new_val = gclient_utils.UpgradeToHttps(new_val)
3207 if new_val != initial:
3208 RunGit(['config', 'rietveld.' + name, new_val])
3209
3210 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3211 SetProperty(settings.GetDefaultPrivateFlag(),
3212 'Private flag (rietveld only)', 'private', False)
3213 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3214 'tree-status-url', False)
3215 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3216 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3217 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3218 'run-post-upload-hook', False)
3219
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003220@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003222 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223
tandrii5d0a0422016-09-14 06:24:35 -07003224 print('WARNING: git cl config works for Rietveld only')
3225 # TODO(tandrii): remove this once we switch to Gerrit.
3226 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003227 parser.add_option('--activate-update', action='store_true',
3228 help='activate auto-updating [rietveld] section in '
3229 '.git/config')
3230 parser.add_option('--deactivate-update', action='store_true',
3231 help='deactivate auto-updating [rietveld] section in '
3232 '.git/config')
3233 options, args = parser.parse_args(args)
3234
3235 if options.deactivate_update:
3236 RunGit(['config', 'rietveld.autoupdate', 'false'])
3237 return
3238
3239 if options.activate_update:
3240 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3241 return
3242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003244 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 return 0
3246
3247 url = args[0]
3248 if not url.endswith('codereview.settings'):
3249 url = os.path.join(url, 'codereview.settings')
3250
3251 # Load code review settings and download hooks (if available).
3252 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3253 return 0
3254
3255
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003256def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003257 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003258 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3259 branch = ShortBranchName(branchref)
3260 _, args = parser.parse_args(args)
3261 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003262 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003263 return RunGit(['config', 'branch.%s.base-url' % branch],
3264 error_ok=False).strip()
3265 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003266 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003267 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3268 error_ok=False).strip()
3269
3270
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003271def color_for_status(status):
3272 """Maps a Changelist status to color, for CMDstatus and other tools."""
3273 return {
3274 'unsent': Fore.RED,
3275 'waiting': Fore.BLUE,
3276 'reply': Fore.YELLOW,
3277 'lgtm': Fore.GREEN,
3278 'commit': Fore.MAGENTA,
3279 'closed': Fore.CYAN,
3280 'error': Fore.WHITE,
3281 }.get(status, Fore.WHITE)
3282
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003283
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003284def get_cl_statuses(changes, fine_grained, max_processes=None):
3285 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003286
3287 If fine_grained is true, this will fetch CL statuses from the server.
3288 Otherwise, simply indicate if there's a matching url for the given branches.
3289
3290 If max_processes is specified, it is used as the maximum number of processes
3291 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3292 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003293
3294 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003295 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003296 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003297 upload.verbosity = 0
3298
3299 if fine_grained:
3300 # Process one branch synchronously to work through authentication, then
3301 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003302 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003303 def fetch(cl):
3304 try:
3305 return (cl, cl.GetStatus())
3306 except:
3307 # See http://crbug.com/629863.
3308 logging.exception('failed to fetch status for %s:', cl)
3309 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003310 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003311
tandriiea9514a2016-08-17 12:32:37 -07003312 changes_to_fetch = changes[1:]
3313 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003314 # Exit early if there was only one branch to fetch.
3315 return
3316
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003317 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003318 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003319 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003320 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003321
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003322 fetched_cls = set()
3323 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003324 while True:
3325 try:
3326 row = it.next(timeout=5)
3327 except multiprocessing.TimeoutError:
3328 break
3329
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003330 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003331 yield row
3332
3333 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003334 for cl in set(changes_to_fetch) - fetched_cls:
3335 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003336
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003337 else:
3338 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003339 for cl in changes:
3340 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003341
rmistry@google.com2dd99862015-06-22 12:22:18 +00003342
3343def upload_branch_deps(cl, args):
3344 """Uploads CLs of local branches that are dependents of the current branch.
3345
3346 If the local branch dependency tree looks like:
3347 test1 -> test2.1 -> test3.1
3348 -> test3.2
3349 -> test2.2 -> test3.3
3350
3351 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3352 run on the dependent branches in this order:
3353 test2.1, test3.1, test3.2, test2.2, test3.3
3354
3355 Note: This function does not rebase your local dependent branches. Use it when
3356 you make a change to the parent branch that will not conflict with its
3357 dependent branches, and you would like their dependencies updated in
3358 Rietveld.
3359 """
3360 if git_common.is_dirty_git_tree('upload-branch-deps'):
3361 return 1
3362
3363 root_branch = cl.GetBranch()
3364 if root_branch is None:
3365 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3366 'Get on a branch!')
3367 if not cl.GetIssue() or not cl.GetPatchset():
3368 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3369 'patchset dependencies without an uploaded CL.')
3370
3371 branches = RunGit(['for-each-ref',
3372 '--format=%(refname:short) %(upstream:short)',
3373 'refs/heads'])
3374 if not branches:
3375 print('No local branches found.')
3376 return 0
3377
3378 # Create a dictionary of all local branches to the branches that are dependent
3379 # on it.
3380 tracked_to_dependents = collections.defaultdict(list)
3381 for b in branches.splitlines():
3382 tokens = b.split()
3383 if len(tokens) == 2:
3384 branch_name, tracked = tokens
3385 tracked_to_dependents[tracked].append(branch_name)
3386
vapiera7fbd5a2016-06-16 09:17:49 -07003387 print()
3388 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003389 dependents = []
3390 def traverse_dependents_preorder(branch, padding=''):
3391 dependents_to_process = tracked_to_dependents.get(branch, [])
3392 padding += ' '
3393 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003394 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003395 dependents.append(dependent)
3396 traverse_dependents_preorder(dependent, padding)
3397 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003398 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003399
3400 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003401 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003402 return 0
3403
vapiera7fbd5a2016-06-16 09:17:49 -07003404 print('This command will checkout all dependent branches and run '
3405 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003406 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3407
andybons@chromium.org962f9462016-02-03 20:00:42 +00003408 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003409 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003410 args.extend(['-t', 'Updated patchset dependency'])
3411
rmistry@google.com2dd99862015-06-22 12:22:18 +00003412 # Record all dependents that failed to upload.
3413 failures = {}
3414 # Go through all dependents, checkout the branch and upload.
3415 try:
3416 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003417 print()
3418 print('--------------------------------------')
3419 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003420 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003421 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003422 try:
3423 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003424 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003425 failures[dependent_branch] = 1
3426 except: # pylint: disable=W0702
3427 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003428 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003429 finally:
3430 # Swap back to the original root branch.
3431 RunGit(['checkout', '-q', root_branch])
3432
vapiera7fbd5a2016-06-16 09:17:49 -07003433 print()
3434 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003435 for dependent_branch in dependents:
3436 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003437 print(' %s : %s' % (dependent_branch, upload_status))
3438 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003439
3440 return 0
3441
3442
kmarshall3bff56b2016-06-06 18:31:47 -07003443def CMDarchive(parser, args):
3444 """Archives and deletes branches associated with closed changelists."""
3445 parser.add_option(
3446 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003447 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003448 parser.add_option(
3449 '-f', '--force', action='store_true',
3450 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003451 parser.add_option(
3452 '-d', '--dry-run', action='store_true',
3453 help='Skip the branch tagging and removal steps.')
3454 parser.add_option(
3455 '-t', '--notags', action='store_true',
3456 help='Do not tag archived branches. '
3457 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003458
3459 auth.add_auth_options(parser)
3460 options, args = parser.parse_args(args)
3461 if args:
3462 parser.error('Unsupported args: %s' % ' '.join(args))
3463 auth_config = auth.extract_auth_config_from_options(options)
3464
3465 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3466 if not branches:
3467 return 0
3468
vapiera7fbd5a2016-06-16 09:17:49 -07003469 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003470 changes = [Changelist(branchref=b, auth_config=auth_config)
3471 for b in branches.splitlines()]
3472 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3473 statuses = get_cl_statuses(changes,
3474 fine_grained=True,
3475 max_processes=options.maxjobs)
3476 proposal = [(cl.GetBranch(),
3477 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3478 for cl, status in statuses
3479 if status == 'closed']
3480 proposal.sort()
3481
3482 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003483 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003484 return 0
3485
3486 current_branch = GetCurrentBranch()
3487
vapiera7fbd5a2016-06-16 09:17:49 -07003488 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003489 if options.notags:
3490 for next_item in proposal:
3491 print(' ' + next_item[0])
3492 else:
3493 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3494 for next_item in proposal:
3495 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003496
kmarshall9249e012016-08-23 12:02:16 -07003497 # Quit now on precondition failure or if instructed by the user, either
3498 # via an interactive prompt or by command line flags.
3499 if options.dry_run:
3500 print('\nNo changes were made (dry run).\n')
3501 return 0
3502 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003503 print('You are currently on a branch \'%s\' which is associated with a '
3504 'closed codereview issue, so archive cannot proceed. Please '
3505 'checkout another branch and run this command again.' %
3506 current_branch)
3507 return 1
kmarshall9249e012016-08-23 12:02:16 -07003508 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003509 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3510 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003511 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003512 return 1
3513
3514 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003515 if not options.notags:
3516 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003517 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003518
vapiera7fbd5a2016-06-16 09:17:49 -07003519 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003520
3521 return 0
3522
3523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003525 """Show status of changelists.
3526
3527 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003528 - Red not sent for review or broken
3529 - Blue waiting for review
3530 - Yellow waiting for you to reply to review
3531 - Green LGTM'ed
3532 - Magenta in the commit queue
3533 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003534
3535 Also see 'git cl comments'.
3536 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003538 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003539 parser.add_option('-f', '--fast', action='store_true',
3540 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003541 parser.add_option(
3542 '-j', '--maxjobs', action='store', type=int,
3543 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003544
3545 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003546 _add_codereview_issue_select_options(
3547 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003548 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003549 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003550 if args:
3551 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003552 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553
iannuccie53c9352016-08-17 14:40:40 -07003554 if options.issue is not None and not options.field:
3555 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003556
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003558 cl = Changelist(auth_config=auth_config, issue=options.issue,
3559 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003560 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003561 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003562 elif options.field == 'id':
3563 issueid = cl.GetIssue()
3564 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003565 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566 elif options.field == 'patch':
3567 patchset = cl.GetPatchset()
3568 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003569 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003570 elif options.field == 'status':
3571 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572 elif options.field == 'url':
3573 url = cl.GetIssueURL()
3574 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003575 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003576 return 0
3577
3578 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3579 if not branches:
3580 print('No local branch found.')
3581 return 0
3582
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003583 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003584 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003585 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003587 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003588 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003589 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003590
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003591 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003592 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3593 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3594 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003595 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003596 c, status = output.next()
3597 branch_statuses[c.GetBranch()] = status
3598 status = branch_statuses.pop(branch)
3599 url = cl.GetIssueURL()
3600 if url and (not status or status == 'error'):
3601 # The issue probably doesn't exist anymore.
3602 url += ' (broken)'
3603
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003604 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003605 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003606 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003607 color = ''
3608 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003609 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003610 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003611 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003612 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003613
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003614 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003615 print()
3616 print('Current branch:',)
3617 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003618 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003619 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003620 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003621 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003622 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003623 print('Issue description:')
3624 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625 return 0
3626
3627
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003628def colorize_CMDstatus_doc():
3629 """To be called once in main() to add colors to git cl status help."""
3630 colors = [i for i in dir(Fore) if i[0].isupper()]
3631
3632 def colorize_line(line):
3633 for color in colors:
3634 if color in line.upper():
3635 # Extract whitespaces first and the leading '-'.
3636 indent = len(line) - len(line.lstrip(' ')) + 1
3637 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3638 return line
3639
3640 lines = CMDstatus.__doc__.splitlines()
3641 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3642
3643
phajdan.jre328cf92016-08-22 04:12:17 -07003644def write_json(path, contents):
3645 with open(path, 'w') as f:
3646 json.dump(contents, f)
3647
3648
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003649@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003650def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003651 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003652
3653 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003654 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003655 parser.add_option('-r', '--reverse', action='store_true',
3656 help='Lookup the branch(es) for the specified issues. If '
3657 'no issues are specified, all branches with mapped '
3658 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003659 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003660 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003661 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003662 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663
dnj@chromium.org406c4402015-03-03 17:22:28 +00003664 if options.reverse:
3665 branches = RunGit(['for-each-ref', 'refs/heads',
3666 '--format=%(refname:short)']).splitlines()
3667
3668 # Reverse issue lookup.
3669 issue_branch_map = {}
3670 for branch in branches:
3671 cl = Changelist(branchref=branch)
3672 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3673 if not args:
3674 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003675 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003676 for issue in args:
3677 if not issue:
3678 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003679 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print('Branch for issue number %s: %s' % (
3681 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003682 if options.json:
3683 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003684 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003685 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003686 if len(args) > 0:
3687 try:
3688 issue = int(args[0])
3689 except ValueError:
3690 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003691 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003692 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003693 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003694 if options.json:
3695 write_json(options.json, {
3696 'issue': cl.GetIssue(),
3697 'issue_url': cl.GetIssueURL(),
3698 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003699 return 0
3700
3701
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003702def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003703 """Shows or posts review comments for any changelist."""
3704 parser.add_option('-a', '--add-comment', dest='comment',
3705 help='comment to add to an issue')
3706 parser.add_option('-i', dest='issue',
3707 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003708 parser.add_option('-j', '--json-file',
3709 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003710 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003711 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003712 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003713
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003714 issue = None
3715 if options.issue:
3716 try:
3717 issue = int(options.issue)
3718 except ValueError:
3719 DieWithError('A review issue id is expected to be a number')
3720
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003721 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003722
3723 if options.comment:
3724 cl.AddComment(options.comment)
3725 return 0
3726
3727 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003728 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003729 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003730 summary.append({
3731 'date': message['date'],
3732 'lgtm': False,
3733 'message': message['text'],
3734 'not_lgtm': False,
3735 'sender': message['sender'],
3736 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003737 if message['disapproval']:
3738 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003739 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003740 elif message['approval']:
3741 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003742 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003743 elif message['sender'] == data['owner_email']:
3744 color = Fore.MAGENTA
3745 else:
3746 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003747 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003748 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003749 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003750 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003751 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003752 if options.json_file:
3753 with open(options.json_file, 'wb') as f:
3754 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003755 return 0
3756
3757
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003758@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003759def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003760 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003761 parser.add_option('-d', '--display', action='store_true',
3762 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003763 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003764 help='New description to set for this issue (- for stdin, '
3765 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003766 parser.add_option('-f', '--force', action='store_true',
3767 help='Delete any unpublished Gerrit edits for this issue '
3768 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003769
3770 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003771 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003772 options, args = parser.parse_args(args)
3773 _process_codereview_select_options(parser, options)
3774
3775 target_issue = None
3776 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003777 target_issue = ParseIssueNumberArgument(args[0])
3778 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003779 parser.print_help()
3780 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003781
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003782 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003783
martiniss6eda05f2016-06-30 10:18:35 -07003784 kwargs = {
3785 'auth_config': auth_config,
3786 'codereview': options.forced_codereview,
3787 }
3788 if target_issue:
3789 kwargs['issue'] = target_issue.issue
3790 if options.forced_codereview == 'rietveld':
3791 kwargs['rietveld_server'] = target_issue.hostname
3792
3793 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003794
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003795 if not cl.GetIssue():
3796 DieWithError('This branch has no associated changelist.')
3797 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003798
smut@google.com34fb6b12015-07-13 20:03:26 +00003799 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003800 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003801 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003802
3803 if options.new_description:
3804 text = options.new_description
3805 if text == '-':
3806 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003807 elif text == '+':
3808 base_branch = cl.GetCommonAncestorWithUpstream()
3809 change = cl.GetChange(base_branch, None, local_description=True)
3810 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003811
3812 description.set_description(text)
3813 else:
3814 description.prompt()
3815
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003816 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003817 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003818 return 0
3819
3820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003821def CreateDescriptionFromLog(args):
3822 """Pulls out the commit log to use as a base for the CL description."""
3823 log_args = []
3824 if len(args) == 1 and not args[0].endswith('.'):
3825 log_args = [args[0] + '..']
3826 elif len(args) == 1 and args[0].endswith('...'):
3827 log_args = [args[0][:-1]]
3828 elif len(args) == 2:
3829 log_args = [args[0] + '..' + args[1]]
3830 else:
3831 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003832 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833
3834
thestig@chromium.org44202a22014-03-11 19:22:18 +00003835def CMDlint(parser, args):
3836 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003837 parser.add_option('--filter', action='append', metavar='-x,+y',
3838 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003839 auth.add_auth_options(parser)
3840 options, args = parser.parse_args(args)
3841 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003842
3843 # Access to a protected member _XX of a client class
3844 # pylint: disable=W0212
3845 try:
3846 import cpplint
3847 import cpplint_chromium
3848 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003849 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003850 return 1
3851
3852 # Change the current working directory before calling lint so that it
3853 # shows the correct base.
3854 previous_cwd = os.getcwd()
3855 os.chdir(settings.GetRoot())
3856 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003857 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003858 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3859 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003860 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003861 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003862 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003863
3864 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003865 command = args + files
3866 if options.filter:
3867 command = ['--filter=' + ','.join(options.filter)] + command
3868 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003869
3870 white_regex = re.compile(settings.GetLintRegex())
3871 black_regex = re.compile(settings.GetLintIgnoreRegex())
3872 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3873 for filename in filenames:
3874 if white_regex.match(filename):
3875 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003876 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003877 else:
3878 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3879 extra_check_functions)
3880 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003881 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003882 finally:
3883 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003884 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003885 if cpplint._cpplint_state.error_count != 0:
3886 return 1
3887 return 0
3888
3889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003890def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003891 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003892 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003894 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003895 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003896 auth.add_auth_options(parser)
3897 options, args = parser.parse_args(args)
3898 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899
sbc@chromium.org71437c02015-04-09 19:29:40 +00003900 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003901 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902 return 1
3903
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003904 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905 if args:
3906 base_branch = args[0]
3907 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003908 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003909 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003911 cl.RunHook(
3912 committing=not options.upload,
3913 may_prompt=False,
3914 verbose=options.verbose,
3915 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003916 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917
3918
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003919def GenerateGerritChangeId(message):
3920 """Returns Ixxxxxx...xxx change id.
3921
3922 Works the same way as
3923 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3924 but can be called on demand on all platforms.
3925
3926 The basic idea is to generate git hash of a state of the tree, original commit
3927 message, author/committer info and timestamps.
3928 """
3929 lines = []
3930 tree_hash = RunGitSilent(['write-tree'])
3931 lines.append('tree %s' % tree_hash.strip())
3932 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3933 if code == 0:
3934 lines.append('parent %s' % parent.strip())
3935 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3936 lines.append('author %s' % author.strip())
3937 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3938 lines.append('committer %s' % committer.strip())
3939 lines.append('')
3940 # Note: Gerrit's commit-hook actually cleans message of some lines and
3941 # whitespace. This code is not doing this, but it clearly won't decrease
3942 # entropy.
3943 lines.append(message)
3944 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3945 stdin='\n'.join(lines))
3946 return 'I%s' % change_hash.strip()
3947
3948
wittman@chromium.org455dc922015-01-26 20:15:50 +00003949def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3950 """Computes the remote branch ref to use for the CL.
3951
3952 Args:
3953 remote (str): The git remote for the CL.
3954 remote_branch (str): The git remote branch for the CL.
3955 target_branch (str): The target branch specified by the user.
3956 pending_prefix (str): The pending prefix from the settings.
3957 """
3958 if not (remote and remote_branch):
3959 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003960
wittman@chromium.org455dc922015-01-26 20:15:50 +00003961 if target_branch:
3962 # Cannonicalize branch references to the equivalent local full symbolic
3963 # refs, which are then translated into the remote full symbolic refs
3964 # below.
3965 if '/' not in target_branch:
3966 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3967 else:
3968 prefix_replacements = (
3969 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3970 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3971 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3972 )
3973 match = None
3974 for regex, replacement in prefix_replacements:
3975 match = re.search(regex, target_branch)
3976 if match:
3977 remote_branch = target_branch.replace(match.group(0), replacement)
3978 break
3979 if not match:
3980 # This is a branch path but not one we recognize; use as-is.
3981 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003982 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3983 # Handle the refs that need to land in different refs.
3984 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003985
wittman@chromium.org455dc922015-01-26 20:15:50 +00003986 # Create the true path to the remote branch.
3987 # Does the following translation:
3988 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3989 # * refs/remotes/origin/master -> refs/heads/master
3990 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3991 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3992 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3993 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3994 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3995 'refs/heads/')
3996 elif remote_branch.startswith('refs/remotes/branch-heads'):
3997 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3998 # If a pending prefix exists then replace refs/ with it.
3999 if pending_prefix:
4000 remote_branch = remote_branch.replace('refs/', pending_prefix)
4001 return remote_branch
4002
4003
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004004def cleanup_list(l):
4005 """Fixes a list so that comma separated items are put as individual items.
4006
4007 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4008 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4009 """
4010 items = sum((i.split(',') for i in l), [])
4011 stripped_items = (i.strip() for i in items)
4012 return sorted(filter(None, stripped_items))
4013
4014
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004015@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004016def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004017 """Uploads the current changelist to codereview.
4018
4019 Can skip dependency patchset uploads for a branch by running:
4020 git config branch.branch_name.skip-deps-uploads True
4021 To unset run:
4022 git config --unset branch.branch_name.skip-deps-uploads
4023 Can also set the above globally by using the --global flag.
4024 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004025 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4026 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004027 parser.add_option('--bypass-watchlists', action='store_true',
4028 dest='bypass_watchlists',
4029 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004030 parser.add_option('-f', action='store_true', dest='force',
4031 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004032 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004033 parser.add_option('-b', '--bug',
4034 help='pre-populate the bug number(s) for this issue. '
4035 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004036 parser.add_option('--message-file', dest='message_file',
4037 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004038 parser.add_option('-t', dest='title',
4039 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004040 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004041 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004042 help='reviewer email addresses')
4043 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004044 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004045 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004046 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004047 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004048 parser.add_option('--emulate_svn_auto_props',
4049 '--emulate-svn-auto-props',
4050 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004051 dest="emulate_svn_auto_props",
4052 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004053 parser.add_option('-c', '--use-commit-queue', action='store_true',
4054 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004055 parser.add_option('--private', action='store_true',
4056 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004057 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004058 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004059 metavar='TARGET',
4060 help='Apply CL to remote ref TARGET. ' +
4061 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004062 parser.add_option('--squash', action='store_true',
4063 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004064 parser.add_option('--no-squash', action='store_true',
4065 help='Don\'t squash multiple commits into one ' +
4066 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004067 parser.add_option('--topic', default=None,
4068 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004069 parser.add_option('--email', default=None,
4070 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004071 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4072 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004073 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4074 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004075 help='Send the patchset to do a CQ dry run right after '
4076 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004077 parser.add_option('--dependencies', action='store_true',
4078 help='Uploads CLs of all the local branches that depend on '
4079 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004080
rmistry@google.com2dd99862015-06-22 12:22:18 +00004081 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004082 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004083 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004084 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004085 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004086 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004087 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004088
sbc@chromium.org71437c02015-04-09 19:29:40 +00004089 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004090 return 1
4091
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004092 options.reviewers = cleanup_list(options.reviewers)
4093 options.cc = cleanup_list(options.cc)
4094
tandriib80458a2016-06-23 12:20:07 -07004095 if options.message_file:
4096 if options.message:
4097 parser.error('only one of --message and --message-file allowed.')
4098 options.message = gclient_utils.FileRead(options.message_file)
4099 options.message_file = None
4100
tandrii4d0545a2016-07-06 03:56:49 -07004101 if options.cq_dry_run and options.use_commit_queue:
4102 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4103
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004104 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4105 settings.GetIsGerrit()
4106
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004107 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004108 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004109
4110
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004111def IsSubmoduleMergeCommit(ref):
4112 # When submodules are added to the repo, we expect there to be a single
4113 # non-git-svn merge commit at remote HEAD with a signature comment.
4114 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004115 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004116 return RunGit(cmd) != ''
4117
4118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004120 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004121
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004122 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4123 upstream and closes the issue automatically and atomically.
4124
4125 Otherwise (in case of Rietveld):
4126 Squashes branch into a single commit.
4127 Updates changelog with metadata (e.g. pointer to review).
4128 Pushes/dcommits the code upstream.
4129 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130 """
4131 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4132 help='bypass upload presubmit hook')
4133 parser.add_option('-m', dest='message',
4134 help="override review description")
4135 parser.add_option('-f', action='store_true', dest='force',
4136 help="force yes to questions (don't prompt)")
4137 parser.add_option('-c', dest='contributor',
4138 help="external contributor for patch (appended to " +
4139 "description and used as author for git). Should be " +
4140 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004141 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004142 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004143 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004144 auth_config = auth.extract_auth_config_from_options(options)
4145
4146 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004147
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004148 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4149 if cl.IsGerrit():
4150 if options.message:
4151 # This could be implemented, but it requires sending a new patch to
4152 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4153 # Besides, Gerrit has the ability to change the commit message on submit
4154 # automatically, thus there is no need to support this option (so far?).
4155 parser.error('-m MESSAGE option is not supported for Gerrit.')
4156 if options.contributor:
4157 parser.error(
4158 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4159 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4160 'the contributor\'s "name <email>". If you can\'t upload such a '
4161 'commit for review, contact your repository admin and request'
4162 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004163 if not cl.GetIssue():
4164 DieWithError('You must upload the issue first to Gerrit.\n'
4165 ' If you would rather have `git cl land` upload '
4166 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004167 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4168 options.verbose)
4169
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004170 current = cl.GetBranch()
4171 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4172 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004173 print()
4174 print('Attempting to push branch %r into another local branch!' % current)
4175 print()
4176 print('Either reparent this branch on top of origin/master:')
4177 print(' git reparent-branch --root')
4178 print()
4179 print('OR run `git rebase-update` if you think the parent branch is ')
4180 print('already committed.')
4181 print()
4182 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004183 return 1
4184
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004185 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004186 # Default to merging against our best guess of the upstream branch.
4187 args = [cl.GetUpstreamBranch()]
4188
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004189 if options.contributor:
4190 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004192 return 1
4193
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004195 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196
sbc@chromium.org71437c02015-04-09 19:29:40 +00004197 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198 return 1
4199
4200 # This rev-list syntax means "show all commits not in my branch that
4201 # are in base_branch".
4202 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4203 base_branch]).splitlines()
4204 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004205 print('Base branch "%s" has %d commits '
4206 'not in this branch.' % (base_branch, len(upstream_commits)))
4207 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208 return 1
4209
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004210 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004211 svn_head = None
4212 if cmd == 'dcommit' or base_has_submodules:
4213 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4214 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004215
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004217 # If the base_head is a submodule merge commit, the first parent of the
4218 # base_head should be a git-svn commit, which is what we're interested in.
4219 base_svn_head = base_branch
4220 if base_has_submodules:
4221 base_svn_head += '^1'
4222
4223 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004225 print('This branch has %d additional commits not upstreamed yet.'
4226 % len(extra_commits.splitlines()))
4227 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4228 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229 return 1
4230
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004231 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004232 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004233 author = None
4234 if options.contributor:
4235 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004236 hook_results = cl.RunHook(
4237 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004238 may_prompt=not options.force,
4239 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004240 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004241 if not hook_results.should_continue():
4242 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004244 # Check the tree status if the tree status URL is set.
4245 status = GetTreeStatus()
4246 if 'closed' == status:
4247 print('The tree is closed. Please wait for it to reopen. Use '
4248 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4249 return 1
4250 elif 'unknown' == status:
4251 print('Unable to determine tree status. Please verify manually and '
4252 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4253 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004254
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004255 change_desc = ChangeDescription(options.message)
4256 if not change_desc.description and cl.GetIssue():
4257 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004259 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004260 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004261 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004262 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004263 print('No description set.')
4264 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004265 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004267 # Keep a separate copy for the commit message, because the commit message
4268 # contains the link to the Rietveld issue, while the Rietveld message contains
4269 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004270 # Keep a separate copy for the commit message.
4271 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004272 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004273
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004274 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004275 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004276 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004277 # after it. Add a period on a new line to circumvent this. Also add a space
4278 # before the period to make sure that Gitiles continues to correctly resolve
4279 # the URL.
4280 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004282 commit_desc.append_footer('Patch from %s.' % options.contributor)
4283
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004284 print('Description:')
4285 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004286
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004287 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004288 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004289 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004290
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004291 # We want to squash all this branch's commits into one commit with the proper
4292 # description. We do this by doing a "reset --soft" to the base branch (which
4293 # keeps the working copy the same), then dcommitting that. If origin/master
4294 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4295 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004297 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4298 # Delete the branches if they exist.
4299 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4300 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4301 result = RunGitWithCode(showref_cmd)
4302 if result[0] == 0:
4303 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304
4305 # We might be in a directory that's present in this branch but not in the
4306 # trunk. Move up to the top of the tree so that git commands that expect a
4307 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004308 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004309 if rel_base_path:
4310 os.chdir(rel_base_path)
4311
4312 # Stuff our change into the merge branch.
4313 # We wrap in a try...finally block so if anything goes wrong,
4314 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004315 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004316 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004317 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004318 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004319 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004320 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004321 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004322 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004323 RunGit(
4324 [
4325 'commit', '--author', options.contributor,
4326 '-m', commit_desc.description,
4327 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004328 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004329 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004330 if base_has_submodules:
4331 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4332 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4333 RunGit(['checkout', CHERRY_PICK_BRANCH])
4334 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004335 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004336 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004337 mirror = settings.GetGitMirror(remote)
4338 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004339 pending_prefix = settings.GetPendingRefPrefix()
4340 if not pending_prefix or branch.startswith(pending_prefix):
4341 # If not using refs/pending/heads/* at all, or target ref is already set
4342 # to pending, then push to the target ref directly.
4343 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004344 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004345 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004346 else:
4347 # Cherry-pick the change on top of pending ref and then push it.
4348 assert branch.startswith('refs/'), branch
4349 assert pending_prefix[-1] == '/', pending_prefix
4350 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004351 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004352 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004353 if retcode == 0:
4354 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355 else:
4356 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004357 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004358 'svn', 'dcommit',
4359 '-C%s' % options.similarity,
4360 '--no-rebase', '--rmdir',
4361 ]
4362 if settings.GetForceHttpsCommitUrl():
4363 # Allow forcing https commit URLs for some projects that don't allow
4364 # committing to http URLs (like Google Code).
4365 remote_url = cl.GetGitSvnRemoteUrl()
4366 if urlparse.urlparse(remote_url).scheme == 'http':
4367 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004368 cmd_args.append('--commit-url=%s' % remote_url)
4369 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004370 if 'Committed r' in output:
4371 revision = re.match(
4372 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4373 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374 finally:
4375 # And then swap back to the original branch and clean up.
4376 RunGit(['checkout', '-q', cl.GetBranch()])
4377 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004378 if base_has_submodules:
4379 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004380
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004381 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004383 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004384
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004385 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004386 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004387 try:
4388 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4389 # We set pushed_to_pending to False, since it made it all the way to the
4390 # real ref.
4391 pushed_to_pending = False
4392 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004393 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004396 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004398 if not to_pending:
4399 if viewvc_url and revision:
4400 change_desc.append_footer(
4401 'Committed: %s%s' % (viewvc_url, revision))
4402 elif revision:
4403 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004404 print('Closing issue '
4405 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004406 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004408 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004409 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004410 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004411 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004412 if options.bypass_hooks:
4413 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4414 else:
4415 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004416 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004417
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004418 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004419 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004420 print('The commit is in the pending queue (%s).' % pending_ref)
4421 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4422 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004423
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004424 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4425 if os.path.isfile(hook):
4426 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004427
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004428 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004429
4430
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004431def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004432 print()
4433 print('Waiting for commit to be landed on %s...' % real_ref)
4434 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004435 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4436 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004437 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004438
4439 loop = 0
4440 while True:
4441 sys.stdout.write('fetching (%d)... \r' % loop)
4442 sys.stdout.flush()
4443 loop += 1
4444
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004445 if mirror:
4446 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004447 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4448 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4449 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4450 for commit in commits.splitlines():
4451 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004452 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004453 return commit
4454
4455 current_rev = to_rev
4456
4457
tandriibf429402016-09-14 07:09:12 -07004458def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004459 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4460
4461 Returns:
4462 (retcode of last operation, output log of last operation).
4463 """
4464 assert pending_ref.startswith('refs/'), pending_ref
4465 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4466 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4467 code = 0
4468 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004469 max_attempts = 3
4470 attempts_left = max_attempts
4471 while attempts_left:
4472 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004473 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004474 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004475
4476 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004477 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004478 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004479 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004480 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004481 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004482 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004483 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004484 continue
4485
4486 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004487 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004488 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004489 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004490 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004491 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4492 'the following files have merge conflicts:' % pending_ref)
4493 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4494 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004495 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004496 return code, out
4497
4498 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004500 code, out = RunGitWithCode(
4501 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4502 if code == 0:
4503 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004504 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004505 return code, out
4506
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004508 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004509 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004510 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004511 print('Fatal push error. Make sure your .netrc credentials and git '
4512 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004513 return code, out
4514
vapiera7fbd5a2016-06-16 09:17:49 -07004515 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004516 return code, out
4517
4518
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004519def IsFatalPushFailure(push_stdout):
4520 """True if retrying push won't help."""
4521 return '(prohibited by Gerrit)' in push_stdout
4522
4523
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004524@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004525def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004526 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004527 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004528 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004529 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004530 message = """This repository appears to be a git-svn mirror, but we
4531don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004532 else:
4533 message = """This doesn't appear to be an SVN repository.
4534If your project has a true, writeable git repository, you probably want to run
4535'git cl land' instead.
4536If your project has a git mirror of an upstream SVN master, you probably need
4537to run 'git svn init'.
4538
4539Using the wrong command might cause your commit to appear to succeed, and the
4540review to be closed, without actually landing upstream. If you choose to
4541proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004542 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004543 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004544 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4545 'Please let us know of this project you are committing to:'
4546 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004547 return SendUpstream(parser, args, 'dcommit')
4548
4549
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004550@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004551def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004552 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004553 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004554 print('This appears to be an SVN repository.')
4555 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004556 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004557 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004558 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004559
4560
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004561@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004562def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004563 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004564 parser.add_option('-b', dest='newbranch',
4565 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004566 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004568 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4569 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004570 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004571 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004572 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004573 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004574 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004575 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004576
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004577
4578 group = optparse.OptionGroup(
4579 parser,
4580 'Options for continuing work on the current issue uploaded from a '
4581 'different clone (e.g. different machine). Must be used independently '
4582 'from the other options. No issue number should be specified, and the '
4583 'branch must have an issue number associated with it')
4584 group.add_option('--reapply', action='store_true', dest='reapply',
4585 help='Reset the branch and reapply the issue.\n'
4586 'CAUTION: This will undo any local changes in this '
4587 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004588
4589 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004590 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004591 parser.add_option_group(group)
4592
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004593 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004594 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004595 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004596 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004597 auth_config = auth.extract_auth_config_from_options(options)
4598
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004599
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004600 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004601 if options.newbranch:
4602 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004603 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004604 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004605
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004606 cl = Changelist(auth_config=auth_config,
4607 codereview=options.forced_codereview)
4608 if not cl.GetIssue():
4609 parser.error('current branch must have an associated issue')
4610
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004611 upstream = cl.GetUpstreamBranch()
4612 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004613 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004614
4615 RunGit(['reset', '--hard', upstream])
4616 if options.pull:
4617 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004618
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004619 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4620 options.directory)
4621
4622 if len(args) != 1 or not args[0]:
4623 parser.error('Must specify issue number or url')
4624
4625 # We don't want uncommitted changes mixed up with the patch.
4626 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004627 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004629 if options.newbranch:
4630 if options.force:
4631 RunGit(['branch', '-D', options.newbranch],
4632 stderr=subprocess2.PIPE, error_ok=True)
4633 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004634 elif not GetCurrentBranch():
4635 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004636
4637 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4638
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004639 if cl.IsGerrit():
4640 if options.reject:
4641 parser.error('--reject is not supported with Gerrit codereview.')
4642 if options.nocommit:
4643 parser.error('--nocommit is not supported with Gerrit codereview.')
4644 if options.directory:
4645 parser.error('--directory is not supported with Gerrit codereview.')
4646
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004647 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004648 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649
4650
4651def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004652 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653 # Provide a wrapper for git svn rebase to help avoid accidental
4654 # git svn dcommit.
4655 # It's the only command that doesn't use parser at all since we just defer
4656 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004657
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004658 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659
4660
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004661def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662 """Fetches the tree status and returns either 'open', 'closed',
4663 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004664 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665 if url:
4666 status = urllib2.urlopen(url).read().lower()
4667 if status.find('closed') != -1 or status == '0':
4668 return 'closed'
4669 elif status.find('open') != -1 or status == '1':
4670 return 'open'
4671 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004672 return 'unset'
4673
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675def GetTreeStatusReason():
4676 """Fetches the tree status from a json url and returns the message
4677 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004678 url = settings.GetTreeStatusUrl()
4679 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680 connection = urllib2.urlopen(json_url)
4681 status = json.loads(connection.read())
4682 connection.close()
4683 return status['message']
4684
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004685
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004686def GetBuilderMaster(bot_list):
4687 """For a given builder, fetch the master from AE if available."""
4688 map_url = 'https://builders-map.appspot.com/'
4689 try:
4690 master_map = json.load(urllib2.urlopen(map_url))
4691 except urllib2.URLError as e:
4692 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4693 (map_url, e))
4694 except ValueError as e:
4695 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4696 if not master_map:
4697 return None, 'Failed to build master map.'
4698
4699 result_master = ''
4700 for bot in bot_list:
4701 builder = bot.split(':', 1)[0]
4702 master_list = master_map.get(builder, [])
4703 if not master_list:
4704 return None, ('No matching master for builder %s.' % builder)
4705 elif len(master_list) > 1:
4706 return None, ('The builder name %s exists in multiple masters %s.' %
4707 (builder, master_list))
4708 else:
4709 cur_master = master_list[0]
4710 if not result_master:
4711 result_master = cur_master
4712 elif result_master != cur_master:
4713 return None, 'The builders do not belong to the same master.'
4714 return result_master, None
4715
4716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004717def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004718 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004719 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004720 status = GetTreeStatus()
4721 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004722 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004723 return 2
4724
vapiera7fbd5a2016-06-16 09:17:49 -07004725 print('The tree is %s' % status)
4726 print()
4727 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004728 if status != 'open':
4729 return 1
4730 return 0
4731
4732
maruel@chromium.org15192402012-09-06 12:38:29 +00004733def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004734 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4735 """
tandrii1838bad2016-10-06 00:10:52 -07004736 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004737 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004738 '-b', '--bot', action='append',
4739 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4740 'times to specify multiple builders. ex: '
4741 '"-b win_rel -b win_layout". See '
4742 'the try server waterfall for the builders name and the tests '
4743 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004744 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004745 '-B', '--bucket', default='',
4746 help=('Buildbucket bucket to send the try requests.'))
4747 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004748 '-m', '--master', default='',
4749 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004750 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004751 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004752 help='Revision to use for the try job; default: the revision will '
4753 'be determined by the try recipe that builder runs, which usually '
4754 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004755 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004756 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004757 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004758 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004759 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004760 '--project',
4761 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004762 'in recipe to determine to which repository or directory to '
4763 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004764 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004765 '-p', '--property', dest='properties', action='append', default=[],
4766 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004767 'key2=value2 etc. The value will be treated as '
4768 'json if decodable, or as string otherwise. '
4769 'NOTE: using this may make your try job not usable for CQ, '
4770 'which will then schedule another try job with default properties')
4771 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004772 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004773 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004774 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004775 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4776 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004777 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004778 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004779 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004780 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004781
machenbach@chromium.org45453142015-09-15 08:45:22 +00004782 # Make sure that all properties are prop=value pairs.
4783 bad_params = [x for x in options.properties if '=' not in x]
4784 if bad_params:
4785 parser.error('Got properties with missing "=": %s' % bad_params)
4786
maruel@chromium.org15192402012-09-06 12:38:29 +00004787 if args:
4788 parser.error('Unknown arguments: %s' % args)
4789
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004790 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004791 if not cl.GetIssue():
4792 parser.error('Need to upload first')
4793
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004794 if cl.IsGerrit():
4795 parser.error(
4796 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4797 'If your project has Commit Queue, dry run is a workaround:\n'
4798 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004799
tandriie113dfd2016-10-11 10:20:12 -07004800 error_message = cl.CannotTriggerTryJobReason()
4801 if error_message:
4802 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004803
maruel@chromium.org15192402012-09-06 12:38:29 +00004804 if not options.name:
4805 options.name = cl.GetBranch()
4806
borenet6c0efe62016-10-19 08:13:29 -07004807 if options.bucket and options.master:
4808 parser.error('Only one of --bucket and --master may be used.')
4809
4810 if options.bot and not options.master and not options.bucket:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004811 options.master, err_msg = GetBuilderMaster(options.bot)
4812 if err_msg:
4813 parser.error('Tryserver master cannot be found because: %s\n'
4814 'Please manually specify the tryserver master'
4815 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004816
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004817 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004818 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004819 if not options.bot:
4820 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004821
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004822 # Get try masters from PRESUBMIT.py files.
4823 masters = presubmit_support.DoGetTryMasters(
4824 change,
4825 change.LocalPaths(),
4826 settings.GetRoot(),
4827 None,
4828 None,
4829 options.verbose,
4830 sys.stdout)
4831 if masters:
4832 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004833
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004834 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4835 options.bot = presubmit_support.DoGetTrySlaves(
4836 change,
4837 change.LocalPaths(),
4838 settings.GetRoot(),
4839 None,
4840 None,
4841 options.verbose,
4842 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004843
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004844 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004845 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004846
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004847 builders_and_tests = {}
4848 # TODO(machenbach): The old style command-line options don't support
4849 # multiple try masters yet.
4850 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4851 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4852
4853 for bot in old_style:
4854 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004855 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004856 elif ',' in bot:
4857 parser.error('Specify one bot per --bot flag')
4858 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004859 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004860
4861 for bot, tests in new_style:
4862 builders_and_tests.setdefault(bot, []).extend(tests)
4863
4864 # Return a master map with one master to be backwards compatible. The
4865 # master name defaults to an empty string, which will cause the master
4866 # not to be set on rietveld (deprecated).
borenet6c0efe62016-10-19 08:13:29 -07004867 bucket = ''
4868 if options.master:
4869 # Add the "master." prefix to the master name to obtain the bucket name.
4870 bucket = _prefix_master(options.master)
4871 return {bucket: builders_and_tests}
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004872
borenet6c0efe62016-10-19 08:13:29 -07004873 if options.bucket:
4874 buckets = {options.bucket: {b: [] for b in options.bot}}
4875 else:
4876 buckets = GetMasterMap()
4877 if not buckets:
4878 # Default to triggering Dry Run (see http://crbug.com/625697).
4879 if options.verbose:
4880 print('git cl try with no bots now defaults to CQ Dry Run.')
4881 try:
4882 cl.SetCQState(_CQState.DRY_RUN)
4883 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4884 return 0
4885 except KeyboardInterrupt:
4886 raise
4887 except:
4888 print('WARNING: failed to trigger CQ Dry Run.\n'
4889 'Either:\n'
4890 ' * your project has no CQ\n'
4891 ' * you don\'t have permission to trigger Dry Run\n'
4892 ' * bug in this code (see stack trace below).\n'
4893 'Consider specifying which bots to trigger manually '
4894 'or asking your project owners for permissions '
4895 'or contacting Chrome Infrastructure team at '
4896 'https://www.chromium.org/infra\n\n')
4897 # Still raise exception so that stack trace is printed.
4898 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004899
borenet6c0efe62016-10-19 08:13:29 -07004900 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004901 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004902 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004903 'of bot requires an initial job from a parent (usually a builder). '
4904 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004905 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004906 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004907
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004908 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004909 if patchset != cl.GetPatchset():
4910 print('Warning: Codereview server has newer patchsets (%s) than most '
4911 'recent upload from local checkout (%s). Did a previous upload '
4912 'fail?\n'
4913 'By default, git cl try uses the latest patchset from '
4914 'codereview, continuing to use patchset %s.\n' %
4915 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004916 try:
borenet6c0efe62016-10-19 08:13:29 -07004917 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4918 patchset)
tandrii568043b2016-10-11 07:49:18 -07004919 except BuildbucketResponseException as ex:
4920 print('ERROR: %s' % ex)
4921 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004922 return 0
4923
4924
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004925def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004926 """Prints info about try jobs associated with current CL."""
4927 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004928 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004929 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004931 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004932 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004933 '--color', action='store_true', default=setup_color.IS_TTY,
4934 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004935 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004936 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4937 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004938 group.add_option(
4939 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004940 parser.add_option_group(group)
4941 auth.add_auth_options(parser)
4942 options, args = parser.parse_args(args)
4943 if args:
4944 parser.error('Unrecognized args: %s' % ' '.join(args))
4945
4946 auth_config = auth.extract_auth_config_from_options(options)
4947 cl = Changelist(auth_config=auth_config)
4948 if not cl.GetIssue():
4949 parser.error('Need to upload first')
4950
tandrii221ab252016-10-06 08:12:04 -07004951 patchset = options.patchset
4952 if not patchset:
4953 patchset = cl.GetMostRecentPatchset()
4954 if not patchset:
4955 parser.error('Codereview doesn\'t know about issue %s. '
4956 'No access to issue or wrong issue number?\n'
4957 'Either upload first, or pass --patchset explicitely' %
4958 cl.GetIssue())
4959
4960 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004961 print('Warning: Codereview server has newer patchsets (%s) than most '
4962 'recent upload from local checkout (%s). Did a previous upload '
4963 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004964 'By default, git cl try-results uses the latest patchset from '
4965 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004966 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004967 try:
tandrii221ab252016-10-06 08:12:04 -07004968 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004969 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004970 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004971 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004972 if options.json:
4973 write_try_results_json(options.json, jobs)
4974 else:
4975 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004976 return 0
4977
4978
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004979@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004980def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004981 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004982 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004983 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004984 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004985
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004986 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004987 if args:
4988 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004989 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004990 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004991 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004992 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004993
4994 # Clear configured merge-base, if there is one.
4995 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004996 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004997 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004998 return 0
4999
5000
thestig@chromium.org00858c82013-12-02 23:08:03 +00005001def CMDweb(parser, args):
5002 """Opens the current CL in the web browser."""
5003 _, args = parser.parse_args(args)
5004 if args:
5005 parser.error('Unrecognized args: %s' % ' '.join(args))
5006
5007 issue_url = Changelist().GetIssueURL()
5008 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005009 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005010 return 1
5011
5012 webbrowser.open(issue_url)
5013 return 0
5014
5015
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005016def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005017 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005018 parser.add_option('-d', '--dry-run', action='store_true',
5019 help='trigger in dry run mode')
5020 parser.add_option('-c', '--clear', action='store_true',
5021 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005022 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005023 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005024 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005025 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005026 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005027 if args:
5028 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005029 if options.dry_run and options.clear:
5030 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5031
iannuccie53c9352016-08-17 14:40:40 -07005032 cl = Changelist(auth_config=auth_config, issue=options.issue,
5033 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005034 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005035 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005036 elif options.dry_run:
5037 state = _CQState.DRY_RUN
5038 else:
5039 state = _CQState.COMMIT
5040 if not cl.GetIssue():
5041 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005042 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005043 return 0
5044
5045
groby@chromium.org411034a2013-02-26 15:12:01 +00005046def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005047 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005048 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005049 auth.add_auth_options(parser)
5050 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005051 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005052 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005053 if args:
5054 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005055 cl = Changelist(auth_config=auth_config, issue=options.issue,
5056 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005057 # Ensure there actually is an issue to close.
5058 cl.GetDescription()
5059 cl.CloseIssue()
5060 return 0
5061
5062
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005063def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005064 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005065 parser.add_option(
5066 '--stat',
5067 action='store_true',
5068 dest='stat',
5069 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005070 auth.add_auth_options(parser)
5071 options, args = parser.parse_args(args)
5072 auth_config = auth.extract_auth_config_from_options(options)
5073 if args:
5074 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005075
5076 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005077 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005078 # Staged changes would be committed along with the patch from last
5079 # upload, hence counted toward the "last upload" side in the final
5080 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005081 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005082 return 1
5083
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005084 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005085 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005086 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005087 if not issue:
5088 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005089 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005090 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005091
5092 # Create a new branch based on the merge-base
5093 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005094 # Clear cached branch in cl object, to avoid overwriting original CL branch
5095 # properties.
5096 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005097 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005098 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005099 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005100 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005101 return rtn
5102
wychen@chromium.org06928532015-02-03 02:11:29 +00005103 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005104 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005105 cmd = ['git', 'diff']
5106 if options.stat:
5107 cmd.append('--stat')
5108 cmd.extend([TMP_BRANCH, branch, '--'])
5109 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005110 finally:
5111 RunGit(['checkout', '-q', branch])
5112 RunGit(['branch', '-D', TMP_BRANCH])
5113
5114 return 0
5115
5116
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005117def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005118 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005119 parser.add_option(
5120 '--no-color',
5121 action='store_true',
5122 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005123 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005124 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005125 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005126
5127 author = RunGit(['config', 'user.email']).strip() or None
5128
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005129 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005130
5131 if args:
5132 if len(args) > 1:
5133 parser.error('Unknown args')
5134 base_branch = args[0]
5135 else:
5136 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005137 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005138
5139 change = cl.GetChange(base_branch, None)
5140 return owners_finder.OwnersFinder(
5141 [f.LocalPath() for f in
5142 cl.GetChange(base_branch, None).AffectedFiles()],
5143 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005144 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005145 disable_color=options.no_color).run()
5146
5147
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005148def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005149 """Generates a diff command."""
5150 # Generate diff for the current branch's changes.
5151 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5152 upstream_commit, '--' ]
5153
5154 if args:
5155 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005156 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005157 diff_cmd.append(arg)
5158 else:
5159 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005160
5161 return diff_cmd
5162
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005163def MatchingFileType(file_name, extensions):
5164 """Returns true if the file name ends with one of the given extensions."""
5165 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005166
enne@chromium.org555cfe42014-01-29 18:21:39 +00005167@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005168def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005169 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005170 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005171 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005172 parser.add_option('--full', action='store_true',
5173 help='Reformat the full content of all touched files')
5174 parser.add_option('--dry-run', action='store_true',
5175 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005176 parser.add_option('--python', action='store_true',
5177 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005178 parser.add_option('--diff', action='store_true',
5179 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005180 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005181
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005182 # git diff generates paths against the root of the repository. Change
5183 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005184 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005185 if rel_base_path:
5186 os.chdir(rel_base_path)
5187
digit@chromium.org29e47272013-05-17 17:01:46 +00005188 # Grab the merge-base commit, i.e. the upstream commit of the current
5189 # branch when it was created or the last time it was rebased. This is
5190 # to cover the case where the user may have called "git fetch origin",
5191 # moving the origin branch to a newer commit, but hasn't rebased yet.
5192 upstream_commit = None
5193 cl = Changelist()
5194 upstream_branch = cl.GetUpstreamBranch()
5195 if upstream_branch:
5196 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5197 upstream_commit = upstream_commit.strip()
5198
5199 if not upstream_commit:
5200 DieWithError('Could not find base commit for this branch. '
5201 'Are you in detached state?')
5202
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005203 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5204 diff_output = RunGit(changed_files_cmd)
5205 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005206 # Filter out files deleted by this CL
5207 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005208
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005209 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5210 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5211 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005212 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005213
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005214 top_dir = os.path.normpath(
5215 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5216
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005217 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5218 # formatted. This is used to block during the presubmit.
5219 return_value = 0
5220
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005221 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005222 # Locate the clang-format binary in the checkout
5223 try:
5224 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005225 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005226 DieWithError(e)
5227
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005228 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005229 cmd = [clang_format_tool]
5230 if not opts.dry_run and not opts.diff:
5231 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005232 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005233 if opts.diff:
5234 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005235 else:
5236 env = os.environ.copy()
5237 env['PATH'] = str(os.path.dirname(clang_format_tool))
5238 try:
5239 script = clang_format.FindClangFormatScriptInChromiumTree(
5240 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005241 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005242 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005243
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005244 cmd = [sys.executable, script, '-p0']
5245 if not opts.dry_run and not opts.diff:
5246 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005247
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005248 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5249 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005250
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005251 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5252 if opts.diff:
5253 sys.stdout.write(stdout)
5254 if opts.dry_run and len(stdout) > 0:
5255 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005256
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005257 # Similar code to above, but using yapf on .py files rather than clang-format
5258 # on C/C++ files
5259 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005260 yapf_tool = gclient_utils.FindExecutable('yapf')
5261 if yapf_tool is None:
5262 DieWithError('yapf not found in PATH')
5263
5264 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005265 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005266 cmd = [yapf_tool]
5267 if not opts.dry_run and not opts.diff:
5268 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005269 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005270 if opts.diff:
5271 sys.stdout.write(stdout)
5272 else:
5273 # TODO(sbc): yapf --lines mode still has some issues.
5274 # https://github.com/google/yapf/issues/154
5275 DieWithError('--python currently only works with --full')
5276
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005277 # Dart's formatter does not have the nice property of only operating on
5278 # modified chunks, so hard code full.
5279 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005280 try:
5281 command = [dart_format.FindDartFmtToolInChromiumTree()]
5282 if not opts.dry_run and not opts.diff:
5283 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005284 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005285
ppi@chromium.org6593d932016-03-03 15:41:15 +00005286 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005287 if opts.dry_run and stdout:
5288 return_value = 2
5289 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005290 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5291 'found in this checkout. Files in other languages are still '
5292 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005293
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005294 # Format GN build files. Always run on full build files for canonical form.
5295 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005296 cmd = ['gn', 'format' ]
5297 if opts.dry_run or opts.diff:
5298 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005299 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005300 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5301 shell=sys.platform == 'win32',
5302 cwd=top_dir)
5303 if opts.dry_run and gn_ret == 2:
5304 return_value = 2 # Not formatted.
5305 elif opts.diff and gn_ret == 2:
5306 # TODO this should compute and print the actual diff.
5307 print("This change has GN build file diff for " + gn_diff_file)
5308 elif gn_ret != 0:
5309 # For non-dry run cases (and non-2 return values for dry-run), a
5310 # nonzero error code indicates a failure, probably because the file
5311 # doesn't parse.
5312 DieWithError("gn format failed on " + gn_diff_file +
5313 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005314
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005315 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005316
5317
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005318@subcommand.usage('<codereview url or issue id>')
5319def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005320 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005321 _, args = parser.parse_args(args)
5322
5323 if len(args) != 1:
5324 parser.print_help()
5325 return 1
5326
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005327 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005328 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005329 parser.print_help()
5330 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005331 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005332
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005333 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005334 output = RunGit(['config', '--local', '--get-regexp',
5335 r'branch\..*\.%s' % issueprefix],
5336 error_ok=True)
5337 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005338 if issue == target_issue:
5339 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005340
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005341 branches = []
5342 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005343 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005344 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005345 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005346 return 1
5347 if len(branches) == 1:
5348 RunGit(['checkout', branches[0]])
5349 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005350 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005351 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005352 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005353 which = raw_input('Choose by index: ')
5354 try:
5355 RunGit(['checkout', branches[int(which)]])
5356 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005357 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005358 return 1
5359
5360 return 0
5361
5362
maruel@chromium.org29404b52014-09-08 22:58:00 +00005363def CMDlol(parser, args):
5364 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005365 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005366 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5367 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5368 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005369 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005370 return 0
5371
5372
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005373class OptionParser(optparse.OptionParser):
5374 """Creates the option parse and add --verbose support."""
5375 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005376 optparse.OptionParser.__init__(
5377 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005378 self.add_option(
5379 '-v', '--verbose', action='count', default=0,
5380 help='Use 2 times for more debugging info')
5381
5382 def parse_args(self, args=None, values=None):
5383 options, args = optparse.OptionParser.parse_args(self, args, values)
5384 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5385 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5386 return options, args
5387
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005389def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005390 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005391 print('\nYour python version %s is unsupported, please upgrade.\n' %
5392 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005393 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005394
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005395 # Reload settings.
5396 global settings
5397 settings = Settings()
5398
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005399 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005400 dispatcher = subcommand.CommandDispatcher(__name__)
5401 try:
5402 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005403 except auth.AuthenticationError as e:
5404 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005405 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005406 if e.code != 500:
5407 raise
5408 DieWithError(
5409 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5410 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005411 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005412
5413
5414if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005415 # These affect sys.stdout so do it outside of main() to simplify mocks in
5416 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005417 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005418 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005419 try:
5420 sys.exit(main(sys.argv[1:]))
5421 except KeyboardInterrupt:
5422 sys.stderr.write('interrupted\n')
5423 sys.exit(1)