blob: c130f3d770a003dc0bfc3404805a428b86bb2a4c [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
skobes6468b902016-10-24 08:45:10 -070043import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
88
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070090 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091 sys.exit(1)
92
93
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000094def GetNoGitPagerEnv():
95 env = os.environ.copy()
96 # 'cat' is a magical git string that disables pagers on all platforms.
97 env['GIT_PAGER'] = 'cat'
98 return env
99
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000100
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000104 except subprocess2.CalledProcessError as e:
105 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 'Command "%s" failed.\n%s' % (
109 ' '.join(args), error_message or e.stdout or ''))
110 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
113def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000115 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000119 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 try:
tandrii5d48c322016-08-18 16:19:37 -0700125 (out, _), code = subprocess2.communicate(['git'] + args,
126 env=GetNoGitPagerEnv(),
127 stdout=subprocess2.PIPE,
128 stderr=stderr)
129 return code, out
130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
132 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
tandrii2a16b952016-10-19 07:09:44 -0700154def time_sleep(seconds):
155 # Use this so that it can be mocked in tests without interfering with python
156 # system machinery.
157 import time # Local import to discourage others from importing time globally.
158 return time.sleep(seconds)
159
160
maruel@chromium.org90541732011-04-01 17:54:18 +0000161def ask_for_data(prompt):
162 try:
163 return raw_input(prompt)
164 except KeyboardInterrupt:
165 # Hide the exception.
166 sys.exit(1)
167
168
tandrii5d48c322016-08-18 16:19:37 -0700169def _git_branch_config_key(branch, key):
170 """Helper method to return Git config key for a branch."""
171 assert branch, 'branch name is required to set git config for it'
172 return 'branch.%s.%s' % (branch, key)
173
174
175def _git_get_branch_config_value(key, default=None, value_type=str,
176 branch=False):
177 """Returns git config value of given or current branch if any.
178
179 Returns default in all other cases.
180 """
181 assert value_type in (int, str, bool)
182 if branch is False: # Distinguishing default arg value from None.
183 branch = GetCurrentBranch()
184
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000185 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700186 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187
tandrii5d48c322016-08-18 16:19:37 -0700188 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700189 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700190 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700191 # git config also has --int, but apparently git config suffers from integer
192 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700193 args.append(_git_branch_config_key(branch, key))
194 code, out = RunGitWithCode(args)
195 if code == 0:
196 value = out.strip()
197 if value_type == int:
198 return int(value)
199 if value_type == bool:
200 return bool(value.lower() == 'true')
201 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 return default
203
204
tandrii5d48c322016-08-18 16:19:37 -0700205def _git_set_branch_config_value(key, value, branch=None, **kwargs):
206 """Sets the value or unsets if it's None of a git branch config.
207
208 Valid, though not necessarily existing, branch must be provided,
209 otherwise currently checked out branch is used.
210 """
211 if not branch:
212 branch = GetCurrentBranch()
213 assert branch, 'a branch name OR currently checked out branch is required'
214 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700215 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700216 if value is None:
217 args.append('--unset')
218 elif isinstance(value, bool):
219 args.append('--bool')
220 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700221 else:
tandrii33a46ff2016-08-23 05:53:40 -0700222 # git config also has --int, but apparently git config suffers from integer
223 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700224 value = str(value)
225 args.append(_git_branch_config_key(branch, key))
226 if value is not None:
227 args.append(value)
228 RunGit(args, **kwargs)
229
230
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000231def add_git_similarity(parser):
232 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700233 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000234 help='Sets the percentage that a pair of files need to match in order to'
235 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000236 parser.add_option(
237 '--find-copies', action='store_true',
238 help='Allows git to look for copies.')
239 parser.add_option(
240 '--no-find-copies', action='store_false', dest='find_copies',
241 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000242
243 old_parser_args = parser.parse_args
244 def Parse(args):
245 options, args = old_parser_args(args)
246
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000247 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.similarity = _git_get_branch_config_value(
249 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 print('Note: Saving similarity of %d%% in git config.'
252 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700253 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 options.similarity = max(0, min(options.similarity, 100))
256
257 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700258 options.find_copies = _git_get_branch_config_value(
259 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000260 else:
tandrii5d48c322016-08-18 16:19:37 -0700261 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 print('Using %d%% similarity for rename/copy detection. '
264 'Override with --similarity.' % options.similarity)
265
266 return options, args
267 parser.parse_args = Parse
268
269
machenbach@chromium.org45453142015-09-15 08:45:22 +0000270def _get_properties_from_options(options):
271 properties = dict(x.split('=', 1) for x in options.properties)
272 for key, val in properties.iteritems():
273 try:
274 properties[key] = json.loads(val)
275 except ValueError:
276 pass # If a value couldn't be evaluated, treat it as a string.
277 return properties
278
279
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280def _prefix_master(master):
281 """Convert user-specified master name to full master name.
282
283 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
284 name, while the developers always use shortened master name
285 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
286 function does the conversion for buildbucket migration.
287 """
borenet6c0efe62016-10-19 08:13:29 -0700288 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 return master
borenet6c0efe62016-10-19 08:13:29 -0700290 return '%s%s' % (MASTER_PREFIX, master)
291
292
293def _unprefix_master(bucket):
294 """Convert bucket name to shortened master name.
295
296 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
297 name, while the developers always use shortened master name
298 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
299 function does the conversion for buildbucket migration.
300 """
301 if bucket.startswith(MASTER_PREFIX):
302 return bucket[len(MASTER_PREFIX):]
303 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306def _buildbucket_retry(operation_name, http, *args, **kwargs):
307 """Retries requests to buildbucket service and returns parsed json content."""
308 try_count = 0
309 while True:
310 response, content = http.request(*args, **kwargs)
311 try:
312 content_json = json.loads(content)
313 except ValueError:
314 content_json = None
315
316 # Buildbucket could return an error even if status==200.
317 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000318 error = content_json.get('error')
319 if error.get('code') == 403:
320 raise BuildbucketResponseException(
321 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000322 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000323 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000324 raise BuildbucketResponseException(msg)
325
326 if response.status == 200:
327 if not content_json:
328 raise BuildbucketResponseException(
329 'Buildbucket returns invalid json content: %s.\n'
330 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
331 content)
332 return content_json
333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content)
335
336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700338 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 try_count += 1
340 assert False, 'unreachable'
341
342
borenet6c0efe62016-10-19 08:13:29 -0700343def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700344 category='git_cl_try', patchset=None):
345 assert changelist.GetIssue(), 'CL must be uploaded first'
346 codereview_url = changelist.GetCodereviewServer()
347 assert codereview_url, 'CL must be uploaded first'
348 patchset = patchset or changelist.GetMostRecentPatchset()
349 assert patchset, 'CL must be uploaded first'
350
351 codereview_host = urlparse.urlparse(codereview_url).hostname
352 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353 http = authenticator.authorize(httplib2.Http())
354 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700355
356 # TODO(tandrii): consider caching Gerrit CL details just like
357 # _RietveldChangelistImpl does, then caching values in these two variables
358 # won't be necessary.
359 owner_email = changelist.GetIssueOwner()
360 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000361
362 buildbucket_put_url = (
363 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000364 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700365 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
366 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
367 hostname=codereview_host,
368 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700370 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371
372 batch_req_body = {'builds': []}
373 print_text = []
374 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700375 for bucket, builders_and_tests in sorted(buckets.iteritems()):
376 print_text.append('Bucket: %s' % bucket)
377 master = None
378 if bucket.startswith(MASTER_PREFIX):
379 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000380 for builder, tests in sorted(builders_and_tests.iteritems()):
381 print_text.append(' %s: %s' % (builder, tests))
382 parameters = {
383 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000384 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700385 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000386 'revision': options.revision,
387 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000388 'properties': {
389 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700390 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700391 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000392 'patch_storage': 'rietveld',
393 'patchset': patchset,
394 'reason': options.name,
tandriide281ae2016-10-12 06:02:30 -0700395 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000396 },
397 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000398 if 'presubmit' in builder.lower():
399 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000400 if tests:
401 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700402 if extra_properties:
403 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000404 if options.clobber:
405 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700406
407 tags = [
408 'builder:%s' % builder,
409 'buildset:%s' % buildset,
410 'user_agent:git_cl_try',
411 ]
412 if master:
413 parameters['properties']['master'] = master
414 tags.append('master:%s' % master)
415
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000416 batch_req_body['builds'].append(
417 {
418 'bucket': bucket,
419 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000420 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700421 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000422 }
423 )
424
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000425 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700426 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000427 http,
428 buildbucket_put_url,
429 'PUT',
430 body=json.dumps(batch_req_body),
431 headers={'Content-Type': 'application/json'}
432 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000433 print_text.append('To see results here, run: git cl try-results')
434 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700435 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000436
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000437
tandrii221ab252016-10-06 08:12:04 -0700438def fetch_try_jobs(auth_config, changelist, buildbucket_host,
439 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700440 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000441
qyearsley53f48a12016-09-01 10:45:13 -0700442 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000443 """
tandrii221ab252016-10-06 08:12:04 -0700444 assert buildbucket_host
445 assert changelist.GetIssue(), 'CL must be uploaded first'
446 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
447 patchset = patchset or changelist.GetMostRecentPatchset()
448 assert patchset, 'CL must be uploaded first'
449
450 codereview_url = changelist.GetCodereviewServer()
451 codereview_host = urlparse.urlparse(codereview_url).hostname
452 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000453 if authenticator.has_cached_credentials():
454 http = authenticator.authorize(httplib2.Http())
455 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700456 print('Warning: Some results might be missing because %s' %
457 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700458 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 http = httplib2.Http()
460
461 http.force_exception_to_status_code = True
462
tandrii221ab252016-10-06 08:12:04 -0700463 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
464 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
465 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000466 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700467 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000468 params = {'tag': 'buildset:%s' % buildset}
469
470 builds = {}
471 while True:
472 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700473 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000474 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700475 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000476 for build in content.get('builds', []):
477 builds[build['id']] = build
478 if 'next_cursor' in content:
479 params['start_cursor'] = content['next_cursor']
480 else:
481 break
482 return builds
483
484
qyearsleyeab3c042016-08-24 09:18:28 -0700485def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486 """Prints nicely result of fetch_try_jobs."""
487 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700488 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000489 return
490
491 # Make a copy, because we'll be modifying builds dictionary.
492 builds = builds.copy()
493 builder_names_cache = {}
494
495 def get_builder(b):
496 try:
497 return builder_names_cache[b['id']]
498 except KeyError:
499 try:
500 parameters = json.loads(b['parameters_json'])
501 name = parameters['builder_name']
502 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700503 print('WARNING: failed to get builder name for build %s: %s' % (
504 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505 name = None
506 builder_names_cache[b['id']] = name
507 return name
508
509 def get_bucket(b):
510 bucket = b['bucket']
511 if bucket.startswith('master.'):
512 return bucket[len('master.'):]
513 return bucket
514
515 if options.print_master:
516 name_fmt = '%%-%ds %%-%ds' % (
517 max(len(str(get_bucket(b))) for b in builds.itervalues()),
518 max(len(str(get_builder(b))) for b in builds.itervalues()))
519 def get_name(b):
520 return name_fmt % (get_bucket(b), get_builder(b))
521 else:
522 name_fmt = '%%-%ds' % (
523 max(len(str(get_builder(b))) for b in builds.itervalues()))
524 def get_name(b):
525 return name_fmt % get_builder(b)
526
527 def sort_key(b):
528 return b['status'], b.get('result'), get_name(b), b.get('url')
529
530 def pop(title, f, color=None, **kwargs):
531 """Pop matching builds from `builds` dict and print them."""
532
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000533 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 colorize = str
535 else:
536 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
537
538 result = []
539 for b in builds.values():
540 if all(b.get(k) == v for k, v in kwargs.iteritems()):
541 builds.pop(b['id'])
542 result.append(b)
543 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700544 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700546 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547
548 total = len(builds)
549 pop(status='COMPLETED', result='SUCCESS',
550 title='Successes:', color=Fore.GREEN,
551 f=lambda b: (get_name(b), b.get('url')))
552 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
553 title='Infra Failures:', color=Fore.MAGENTA,
554 f=lambda b: (get_name(b), b.get('url')))
555 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
556 title='Failures:', color=Fore.RED,
557 f=lambda b: (get_name(b), b.get('url')))
558 pop(status='COMPLETED', result='CANCELED',
559 title='Canceled:', color=Fore.MAGENTA,
560 f=lambda b: (get_name(b),))
561 pop(status='COMPLETED', result='FAILURE',
562 failure_reason='INVALID_BUILD_DEFINITION',
563 title='Wrong master/builder name:', color=Fore.MAGENTA,
564 f=lambda b: (get_name(b),))
565 pop(status='COMPLETED', result='FAILURE',
566 title='Other failures:',
567 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
568 pop(status='COMPLETED',
569 title='Other finished:',
570 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
571 pop(status='STARTED',
572 title='Started:', color=Fore.YELLOW,
573 f=lambda b: (get_name(b), b.get('url')))
574 pop(status='SCHEDULED',
575 title='Scheduled:',
576 f=lambda b: (get_name(b), 'id=%s' % b['id']))
577 # The last section is just in case buildbucket API changes OR there is a bug.
578 pop(title='Other:',
579 f=lambda b: (get_name(b), 'id=%s' % b['id']))
580 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700581 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582
583
qyearsley53f48a12016-09-01 10:45:13 -0700584def write_try_results_json(output_file, builds):
585 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
586
587 The input |builds| dict is assumed to be generated by Buildbucket.
588 Buildbucket documentation: http://goo.gl/G0s101
589 """
590
591 def convert_build_dict(build):
592 return {
593 'buildbucket_id': build.get('id'),
594 'status': build.get('status'),
595 'result': build.get('result'),
596 'bucket': build.get('bucket'),
597 'builder_name': json.loads(
598 build.get('parameters_json', '{}')).get('builder_name'),
599 'failure_reason': build.get('failure_reason'),
600 'url': build.get('url'),
601 }
602
603 converted = []
604 for _, build in sorted(builds.items()):
605 converted.append(convert_build_dict(build))
606 write_json(output_file, converted)
607
608
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000609def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
610 """Return the corresponding git ref if |base_url| together with |glob_spec|
611 matches the full |url|.
612
613 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
614 """
615 fetch_suburl, as_ref = glob_spec.split(':')
616 if allow_wildcards:
617 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
618 if glob_match:
619 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
620 # "branches/{472,597,648}/src:refs/remotes/svn/*".
621 branch_re = re.escape(base_url)
622 if glob_match.group(1):
623 branch_re += '/' + re.escape(glob_match.group(1))
624 wildcard = glob_match.group(2)
625 if wildcard == '*':
626 branch_re += '([^/]*)'
627 else:
628 # Escape and replace surrounding braces with parentheses and commas
629 # with pipe symbols.
630 wildcard = re.escape(wildcard)
631 wildcard = re.sub('^\\\\{', '(', wildcard)
632 wildcard = re.sub('\\\\,', '|', wildcard)
633 wildcard = re.sub('\\\\}$', ')', wildcard)
634 branch_re += wildcard
635 if glob_match.group(3):
636 branch_re += re.escape(glob_match.group(3))
637 match = re.match(branch_re, url)
638 if match:
639 return re.sub('\*$', match.group(1), as_ref)
640
641 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
642 if fetch_suburl:
643 full_url = base_url + '/' + fetch_suburl
644 else:
645 full_url = base_url
646 if full_url == url:
647 return as_ref
648 return None
649
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000650
iannucci@chromium.org79540052012-10-19 23:15:26 +0000651def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000652 """Prints statistics about the change to the user."""
653 # --no-ext-diff is broken in some versions of Git, so try to work around
654 # this by overriding the environment (but there is still a problem if the
655 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000656 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000657 if 'GIT_EXTERNAL_DIFF' in env:
658 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000659
660 if find_copies:
661 similarity_options = ['--find-copies-harder', '-l100000',
662 '-C%s' % similarity]
663 else:
664 similarity_options = ['-M%s' % similarity]
665
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000666 try:
667 stdout = sys.stdout.fileno()
668 except AttributeError:
669 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000670 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000671 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000672 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000673 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000674
675
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000676class BuildbucketResponseException(Exception):
677 pass
678
679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680class Settings(object):
681 def __init__(self):
682 self.default_server = None
683 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000684 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 self.is_git_svn = None
686 self.svn_branch = None
687 self.tree_status_url = None
688 self.viewvc_url = None
689 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000690 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000691 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000692 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000693 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000694 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000695 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000696 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700697 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698
699 def LazyUpdateIfNeeded(self):
700 """Updates the settings from a codereview.settings file, if available."""
701 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000702 # The only value that actually changes the behavior is
703 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000704 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000705 error_ok=True
706 ).strip().lower()
707
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000709 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 LoadCodereviewSettingsFromFile(cr_settings_file)
711 self.updated = True
712
713 def GetDefaultServerUrl(self, error_ok=False):
714 if not self.default_server:
715 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000716 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000717 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 if error_ok:
719 return self.default_server
720 if not self.default_server:
721 error_message = ('Could not find settings file. You must configure '
722 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000723 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000724 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 return self.default_server
726
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000727 @staticmethod
728 def GetRelativeRoot():
729 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000732 if self.root is None:
733 self.root = os.path.abspath(self.GetRelativeRoot())
734 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000736 def GetGitMirror(self, remote='origin'):
737 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000738 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000739 if not os.path.isdir(local_url):
740 return None
741 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
742 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
743 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
744 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
745 if mirror.exists():
746 return mirror
747 return None
748
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749 def GetIsGitSvn(self):
750 """Return true if this repo looks like it's using git-svn."""
751 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000752 if self.GetPendingRefPrefix():
753 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
754 self.is_git_svn = False
755 else:
756 # If you have any "svn-remote.*" config keys, we think you're using svn.
757 self.is_git_svn = RunGitWithCode(
758 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000759 return self.is_git_svn
760
761 def GetSVNBranch(self):
762 if self.svn_branch is None:
763 if not self.GetIsGitSvn():
764 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
765
766 # Try to figure out which remote branch we're based on.
767 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000768 # 1) iterate through our branch history and find the svn URL.
769 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770
771 # regexp matching the git-svn line that contains the URL.
772 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
773
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000774 # We don't want to go through all of history, so read a line from the
775 # pipe at a time.
776 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000777 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000778 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
779 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000780 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000781 for line in proc.stdout:
782 match = git_svn_re.match(line)
783 if match:
784 url = match.group(1)
785 proc.stdout.close() # Cut pipe.
786 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000788 if url:
789 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
790 remotes = RunGit(['config', '--get-regexp',
791 r'^svn-remote\..*\.url']).splitlines()
792 for remote in remotes:
793 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000795 remote = match.group(1)
796 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000797 rewrite_root = RunGit(
798 ['config', 'svn-remote.%s.rewriteRoot' % remote],
799 error_ok=True).strip()
800 if rewrite_root:
801 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000802 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000803 ['config', 'svn-remote.%s.fetch' % remote],
804 error_ok=True).strip()
805 if fetch_spec:
806 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
807 if self.svn_branch:
808 break
809 branch_spec = RunGit(
810 ['config', 'svn-remote.%s.branches' % remote],
811 error_ok=True).strip()
812 if branch_spec:
813 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
814 if self.svn_branch:
815 break
816 tag_spec = RunGit(
817 ['config', 'svn-remote.%s.tags' % remote],
818 error_ok=True).strip()
819 if tag_spec:
820 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
821 if self.svn_branch:
822 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823
824 if not self.svn_branch:
825 DieWithError('Can\'t guess svn branch -- try specifying it on the '
826 'command line')
827
828 return self.svn_branch
829
830 def GetTreeStatusUrl(self, error_ok=False):
831 if not self.tree_status_url:
832 error_message = ('You must configure your tree status URL by running '
833 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000834 self.tree_status_url = self._GetRietveldConfig(
835 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 return self.tree_status_url
837
838 def GetViewVCUrl(self):
839 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 return self.viewvc_url
842
rmistry@google.com90752582014-01-14 21:04:50 +0000843 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000844 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000845
rmistry@google.com78948ed2015-07-08 23:09:57 +0000846 def GetIsSkipDependencyUpload(self, branch_name):
847 """Returns true if specified branch should skip dep uploads."""
848 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
849 error_ok=True)
850
rmistry@google.com5626a922015-02-26 14:03:30 +0000851 def GetRunPostUploadHook(self):
852 run_post_upload_hook = self._GetRietveldConfig(
853 'run-post-upload-hook', error_ok=True)
854 return run_post_upload_hook == "True"
855
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000856 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000857 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000858
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000859 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000860 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000861
ukai@chromium.orge8077812012-02-03 03:41:46 +0000862 def GetIsGerrit(self):
863 """Return true if this repo is assosiated with gerrit code review system."""
864 if self.is_gerrit is None:
865 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
866 return self.is_gerrit
867
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000868 def GetSquashGerritUploads(self):
869 """Return true if uploads to Gerrit should be squashed by default."""
870 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700871 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
872 if self.squash_gerrit_uploads is None:
873 # Default is squash now (http://crbug.com/611892#c23).
874 self.squash_gerrit_uploads = not (
875 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
876 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000877 return self.squash_gerrit_uploads
878
tandriia60502f2016-06-20 02:01:53 -0700879 def GetSquashGerritUploadsOverride(self):
880 """Return True or False if codereview.settings should be overridden.
881
882 Returns None if no override has been defined.
883 """
884 # See also http://crbug.com/611892#c23
885 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
886 error_ok=True).strip()
887 if result == 'true':
888 return True
889 if result == 'false':
890 return False
891 return None
892
tandrii@chromium.org28253532016-04-14 13:46:56 +0000893 def GetGerritSkipEnsureAuthenticated(self):
894 """Return True if EnsureAuthenticated should not be done for Gerrit
895 uploads."""
896 if self.gerrit_skip_ensure_authenticated is None:
897 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000898 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000899 error_ok=True).strip() == 'true')
900 return self.gerrit_skip_ensure_authenticated
901
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000902 def GetGitEditor(self):
903 """Return the editor specified in the git config, or None if none is."""
904 if self.git_editor is None:
905 self.git_editor = self._GetConfig('core.editor', error_ok=True)
906 return self.git_editor or None
907
thestig@chromium.org44202a22014-03-11 19:22:18 +0000908 def GetLintRegex(self):
909 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
910 DEFAULT_LINT_REGEX)
911
912 def GetLintIgnoreRegex(self):
913 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
914 DEFAULT_LINT_IGNORE_REGEX)
915
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000916 def GetProject(self):
917 if not self.project:
918 self.project = self._GetRietveldConfig('project', error_ok=True)
919 return self.project
920
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000921 def GetForceHttpsCommitUrl(self):
922 if not self.force_https_commit_url:
923 self.force_https_commit_url = self._GetRietveldConfig(
924 'force-https-commit-url', error_ok=True)
925 return self.force_https_commit_url
926
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000927 def GetPendingRefPrefix(self):
928 if not self.pending_ref_prefix:
929 self.pending_ref_prefix = self._GetRietveldConfig(
930 'pending-ref-prefix', error_ok=True)
931 return self.pending_ref_prefix
932
tandriif46c20f2016-09-14 06:17:05 -0700933 def GetHasGitNumberFooter(self):
934 # TODO(tandrii): this has to be removed after Rietveld is read-only.
935 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
936 if not self.git_number_footer:
937 self.git_number_footer = self._GetRietveldConfig(
938 'git-number-footer', error_ok=True)
939 return self.git_number_footer
940
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000941 def _GetRietveldConfig(self, param, **kwargs):
942 return self._GetConfig('rietveld.' + param, **kwargs)
943
rmistry@google.com78948ed2015-07-08 23:09:57 +0000944 def _GetBranchConfig(self, branch_name, param, **kwargs):
945 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 def _GetConfig(self, param, **kwargs):
948 self.LazyUpdateIfNeeded()
949 return RunGit(['config', param], **kwargs).strip()
950
951
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952def ShortBranchName(branch):
953 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000954 return branch.replace('refs/heads/', '', 1)
955
956
957def GetCurrentBranchRef():
958 """Returns branch ref (e.g., refs/heads/master) or None."""
959 return RunGit(['symbolic-ref', 'HEAD'],
960 stderr=subprocess2.VOID, error_ok=True).strip() or None
961
962
963def GetCurrentBranch():
964 """Returns current branch or None.
965
966 For refs/heads/* branches, returns just last part. For others, full ref.
967 """
968 branchref = GetCurrentBranchRef()
969 if branchref:
970 return ShortBranchName(branchref)
971 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
973
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000974class _CQState(object):
975 """Enum for states of CL with respect to Commit Queue."""
976 NONE = 'none'
977 DRY_RUN = 'dry_run'
978 COMMIT = 'commit'
979
980 ALL_STATES = [NONE, DRY_RUN, COMMIT]
981
982
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000983class _ParsedIssueNumberArgument(object):
984 def __init__(self, issue=None, patchset=None, hostname=None):
985 self.issue = issue
986 self.patchset = patchset
987 self.hostname = hostname
988
989 @property
990 def valid(self):
991 return self.issue is not None
992
993
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000994def ParseIssueNumberArgument(arg):
995 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
996 fail_result = _ParsedIssueNumberArgument()
997
998 if arg.isdigit():
999 return _ParsedIssueNumberArgument(issue=int(arg))
1000 if not arg.startswith('http'):
1001 return fail_result
1002 url = gclient_utils.UpgradeToHttps(arg)
1003 try:
1004 parsed_url = urlparse.urlparse(url)
1005 except ValueError:
1006 return fail_result
1007 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1008 tmp = cls.ParseIssueURL(parsed_url)
1009 if tmp is not None:
1010 return tmp
1011 return fail_result
1012
1013
tandriic2405f52016-10-10 08:13:15 -07001014class GerritIssueNotExists(Exception):
1015 def __init__(self, issue, url):
1016 self.issue = issue
1017 self.url = url
1018 super(GerritIssueNotExists, self).__init__()
1019
1020 def __str__(self):
1021 return 'issue %s at %s does not exist or you have no access to it' % (
1022 self.issue, self.url)
1023
1024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 """Changelist works with one changelist in local branch.
1027
1028 Supports two codereview backends: Rietveld or Gerrit, selected at object
1029 creation.
1030
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001031 Notes:
1032 * Not safe for concurrent multi-{thread,process} use.
1033 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001034 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001035 """
1036
1037 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1038 """Create a new ChangeList instance.
1039
1040 If issue is given, the codereview must be given too.
1041
1042 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1043 Otherwise, it's decided based on current configuration of the local branch,
1044 with default being 'rietveld' for backwards compatibility.
1045 See _load_codereview_impl for more details.
1046
1047 **kwargs will be passed directly to codereview implementation.
1048 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001050 global settings
1051 if not settings:
1052 # Happens when git_cl.py is used as a utility library.
1053 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001054
1055 if issue:
1056 assert codereview, 'codereview must be known, if issue is known'
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 self.branchref = branchref
1059 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001060 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 self.branch = ShortBranchName(self.branchref)
1062 else:
1063 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001065 self.lookedup_issue = False
1066 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 self.has_description = False
1068 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001069 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001071 self.cc = None
1072 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001073 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001074
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001075 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001076 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001077 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001078 assert self._codereview_impl
1079 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001080
1081 def _load_codereview_impl(self, codereview=None, **kwargs):
1082 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001083 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1084 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1085 self._codereview = codereview
1086 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001087 return
1088
1089 # Automatic selection based on issue number set for a current branch.
1090 # Rietveld takes precedence over Gerrit.
1091 assert not self.issue
1092 # Whether we find issue or not, we are doing the lookup.
1093 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001094 if self.GetBranch():
1095 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1096 issue = _git_get_branch_config_value(
1097 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1098 if issue:
1099 self._codereview = codereview
1100 self._codereview_impl = cls(self, **kwargs)
1101 self.issue = int(issue)
1102 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001103
1104 # No issue is set for this branch, so decide based on repo-wide settings.
1105 return self._load_codereview_impl(
1106 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1107 **kwargs)
1108
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001109 def IsGerrit(self):
1110 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001111
1112 def GetCCList(self):
1113 """Return the users cc'd on this CL.
1114
agable92bec4f2016-08-24 09:27:27 -07001115 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001116 """
1117 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001118 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001119 more_cc = ','.join(self.watchers)
1120 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1121 return self.cc
1122
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001123 def GetCCListWithoutDefault(self):
1124 """Return the users cc'd on this CL excluding default ones."""
1125 if self.cc is None:
1126 self.cc = ','.join(self.watchers)
1127 return self.cc
1128
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001129 def SetWatchers(self, watchers):
1130 """Set the list of email addresses that should be cc'd based on the changed
1131 files in this CL.
1132 """
1133 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134
1135 def GetBranch(self):
1136 """Returns the short branch name, e.g. 'master'."""
1137 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001138 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001139 if not branchref:
1140 return None
1141 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 self.branch = ShortBranchName(self.branchref)
1143 return self.branch
1144
1145 def GetBranchRef(self):
1146 """Returns the full branch name, e.g. 'refs/heads/master'."""
1147 self.GetBranch() # Poke the lazy loader.
1148 return self.branchref
1149
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001150 def ClearBranch(self):
1151 """Clears cached branch data of this object."""
1152 self.branch = self.branchref = None
1153
tandrii5d48c322016-08-18 16:19:37 -07001154 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1155 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1156 kwargs['branch'] = self.GetBranch()
1157 return _git_get_branch_config_value(key, default, **kwargs)
1158
1159 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1160 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1161 assert self.GetBranch(), (
1162 'this CL must have an associated branch to %sset %s%s' %
1163 ('un' if value is None else '',
1164 key,
1165 '' if value is None else ' to %r' % value))
1166 kwargs['branch'] = self.GetBranch()
1167 return _git_set_branch_config_value(key, value, **kwargs)
1168
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 @staticmethod
1170 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001171 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 e.g. 'origin', 'refs/heads/master'
1173 """
1174 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001175 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001178 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001180 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1181 error_ok=True).strip()
1182 if upstream_branch:
1183 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001185 # Fall back on trying a git-svn upstream branch.
1186 if settings.GetIsGitSvn():
1187 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001189 # Else, try to guess the origin remote.
1190 remote_branches = RunGit(['branch', '-r']).split()
1191 if 'origin/master' in remote_branches:
1192 # Fall back on origin/master if it exits.
1193 remote = 'origin'
1194 upstream_branch = 'refs/heads/master'
1195 elif 'origin/trunk' in remote_branches:
1196 # Fall back on origin/trunk if it exists. Generally a shared
1197 # git-svn clone
1198 remote = 'origin'
1199 upstream_branch = 'refs/heads/trunk'
1200 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001201 DieWithError(
1202 'Unable to determine default branch to diff against.\n'
1203 'Either pass complete "git diff"-style arguments, like\n'
1204 ' git cl upload origin/master\n'
1205 'or verify this branch is set up to track another \n'
1206 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207
1208 return remote, upstream_branch
1209
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001210 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001211 upstream_branch = self.GetUpstreamBranch()
1212 if not BranchExists(upstream_branch):
1213 DieWithError('The upstream for the current branch (%s) does not exist '
1214 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001215 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001216 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 def GetUpstreamBranch(self):
1219 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001222 upstream_branch = upstream_branch.replace('refs/heads/',
1223 'refs/remotes/%s/' % remote)
1224 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1225 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 self.upstream_branch = upstream_branch
1227 return self.upstream_branch
1228
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001229 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001230 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 remote, branch = None, self.GetBranch()
1232 seen_branches = set()
1233 while branch not in seen_branches:
1234 seen_branches.add(branch)
1235 remote, branch = self.FetchUpstreamTuple(branch)
1236 branch = ShortBranchName(branch)
1237 if remote != '.' or branch.startswith('refs/remotes'):
1238 break
1239 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001240 remotes = RunGit(['remote'], error_ok=True).split()
1241 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001243 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001244 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001245 logging.warning('Could not determine which remote this change is '
1246 'associated with, so defaulting to "%s". This may '
1247 'not be what you want. You may prevent this message '
1248 'by running "git svn info" as documented here: %s',
1249 self._remote,
1250 GIT_INSTRUCTIONS_URL)
1251 else:
1252 logging.warn('Could not determine which remote this change is '
1253 'associated with. You may prevent this message by '
1254 'running "git svn info" as documented here: %s',
1255 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001256 branch = 'HEAD'
1257 if branch.startswith('refs/remotes'):
1258 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001259 elif branch.startswith('refs/branch-heads/'):
1260 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 else:
1262 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 return self._remote
1264
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001265 def GitSanityChecks(self, upstream_git_obj):
1266 """Checks git repo status and ensures diff is from local commits."""
1267
sbc@chromium.org79706062015-01-14 21:18:12 +00001268 if upstream_git_obj is None:
1269 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001270 print('ERROR: unable to determine current branch (detached HEAD?)',
1271 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001272 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001273 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001274 return False
1275
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001276 # Verify the commit we're diffing against is in our current branch.
1277 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1278 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1279 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001280 print('ERROR: %s is not in the current branch. You may need to rebase '
1281 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001282 return False
1283
1284 # List the commits inside the diff, and verify they are all local.
1285 commits_in_diff = RunGit(
1286 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1287 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1288 remote_branch = remote_branch.strip()
1289 if code != 0:
1290 _, remote_branch = self.GetRemoteBranch()
1291
1292 commits_in_remote = RunGit(
1293 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1294
1295 common_commits = set(commits_in_diff) & set(commits_in_remote)
1296 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001297 print('ERROR: Your diff contains %d commits already in %s.\n'
1298 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1299 'the diff. If you are using a custom git flow, you can override'
1300 ' the reference used for this check with "git config '
1301 'gitcl.remotebranch <git-ref>".' % (
1302 len(common_commits), remote_branch, upstream_git_obj),
1303 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001304 return False
1305 return True
1306
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001307 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001308 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001309
1310 Returns None if it is not set.
1311 """
tandrii5d48c322016-08-18 16:19:37 -07001312 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001314 def GetGitSvnRemoteUrl(self):
1315 """Return the configured git-svn remote URL parsed from git svn info.
1316
1317 Returns None if it is not set.
1318 """
1319 # URL is dependent on the current directory.
1320 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1321 if data:
1322 keys = dict(line.split(': ', 1) for line in data.splitlines()
1323 if ': ' in line)
1324 return keys.get('URL', None)
1325 return None
1326
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 def GetRemoteUrl(self):
1328 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1329
1330 Returns None if there is no remote.
1331 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001332 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001333 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1334
1335 # If URL is pointing to a local directory, it is probably a git cache.
1336 if os.path.isdir(url):
1337 url = RunGit(['config', 'remote.%s.url' % remote],
1338 error_ok=True,
1339 cwd=url).strip()
1340 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001342 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001343 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001344 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001345 self.issue = self._GitGetBranchConfigValue(
1346 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 return self.issue
1349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 def GetIssueURL(self):
1351 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001352 issue = self.GetIssue()
1353 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001354 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001355 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356
1357 def GetDescription(self, pretty=False):
1358 if not self.has_description:
1359 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 self.has_description = True
1362 if pretty:
1363 wrapper = textwrap.TextWrapper()
1364 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1365 return wrapper.fill(self.description)
1366 return self.description
1367
1368 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001369 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001370 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001371 self.patchset = self._GitGetBranchConfigValue(
1372 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001373 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 return self.patchset
1375
1376 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001377 """Set this branch's patchset. If patchset=0, clears the patchset."""
1378 assert self.GetBranch()
1379 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001380 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001381 else:
1382 self.patchset = int(patchset)
1383 self._GitSetBranchConfigValue(
1384 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001386 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001387 """Set this branch's issue. If issue isn't given, clears the issue."""
1388 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001390 issue = int(issue)
1391 self._GitSetBranchConfigValue(
1392 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001393 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001394 codereview_server = self._codereview_impl.GetCodereviewServer()
1395 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001396 self._GitSetBranchConfigValue(
1397 self._codereview_impl.CodereviewServerConfigKey(),
1398 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 else:
tandrii5d48c322016-08-18 16:19:37 -07001400 # Reset all of these just to be clean.
1401 reset_suffixes = [
1402 'last-upload-hash',
1403 self._codereview_impl.IssueConfigKey(),
1404 self._codereview_impl.PatchsetConfigKey(),
1405 self._codereview_impl.CodereviewServerConfigKey(),
1406 ] + self._PostUnsetIssueProperties()
1407 for prop in reset_suffixes:
1408 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001410 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
dnjba1b0f32016-09-02 12:37:42 -07001412 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001413 if not self.GitSanityChecks(upstream_branch):
1414 DieWithError('\nGit sanity check failure')
1415
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001416 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001417 if not root:
1418 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001419 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001420
1421 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001422 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001423 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001424 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001425 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001426 except subprocess2.CalledProcessError:
1427 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001428 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001429 'This branch probably doesn\'t exist anymore. To reset the\n'
1430 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001431 ' git branch --set-upstream-to origin/master %s\n'
1432 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001433 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001434
maruel@chromium.org52424302012-08-29 15:14:30 +00001435 issue = self.GetIssue()
1436 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001437 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001438 description = self.GetDescription()
1439 else:
1440 # If the change was never uploaded, use the log messages of all commits
1441 # up to the branch point, as git cl upload will prefill the description
1442 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001443 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1444 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001445
1446 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001447 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001448 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001449 name,
1450 description,
1451 absroot,
1452 files,
1453 issue,
1454 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001455 author,
1456 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001457
dsansomee2d6fd92016-09-08 00:10:47 -07001458 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001459 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001460 return self._codereview_impl.UpdateDescriptionRemote(
1461 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001462
1463 def RunHook(self, committing, may_prompt, verbose, change):
1464 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1465 try:
1466 return presubmit_support.DoPresubmitChecks(change, committing,
1467 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1468 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001469 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1470 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001471 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001472 DieWithError(
1473 ('%s\nMaybe your depot_tools is out of date?\n'
1474 'If all fails, contact maruel@') % e)
1475
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001476 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1477 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001478 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1479 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001480 else:
1481 # Assume url.
1482 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1483 urlparse.urlparse(issue_arg))
1484 if not parsed_issue_arg or not parsed_issue_arg.valid:
1485 DieWithError('Failed to parse issue argument "%s". '
1486 'Must be an issue number or a valid URL.' % issue_arg)
1487 return self._codereview_impl.CMDPatchWithParsedIssue(
1488 parsed_issue_arg, reject, nocommit, directory)
1489
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001490 def CMDUpload(self, options, git_diff_args, orig_args):
1491 """Uploads a change to codereview."""
1492 if git_diff_args:
1493 # TODO(ukai): is it ok for gerrit case?
1494 base_branch = git_diff_args[0]
1495 else:
1496 if self.GetBranch() is None:
1497 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1498
1499 # Default to diffing against common ancestor of upstream branch
1500 base_branch = self.GetCommonAncestorWithUpstream()
1501 git_diff_args = [base_branch, 'HEAD']
1502
1503 # Make sure authenticated to codereview before running potentially expensive
1504 # hooks. It is a fast, best efforts check. Codereview still can reject the
1505 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001506 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001507
1508 # Apply watchlists on upload.
1509 change = self.GetChange(base_branch, None)
1510 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1511 files = [f.LocalPath() for f in change.AffectedFiles()]
1512 if not options.bypass_watchlists:
1513 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1514
1515 if not options.bypass_hooks:
1516 if options.reviewers or options.tbr_owners:
1517 # Set the reviewer list now so that presubmit checks can access it.
1518 change_description = ChangeDescription(change.FullDescriptionText())
1519 change_description.update_reviewers(options.reviewers,
1520 options.tbr_owners,
1521 change)
1522 change.SetDescriptionText(change_description.description)
1523 hook_results = self.RunHook(committing=False,
1524 may_prompt=not options.force,
1525 verbose=options.verbose,
1526 change=change)
1527 if not hook_results.should_continue():
1528 return 1
1529 if not options.reviewers and hook_results.reviewers:
1530 options.reviewers = hook_results.reviewers.split(',')
1531
1532 if self.GetIssue():
1533 latest_patchset = self.GetMostRecentPatchset()
1534 local_patchset = self.GetPatchset()
1535 if (latest_patchset and local_patchset and
1536 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001537 print('The last upload made from this repository was patchset #%d but '
1538 'the most recent patchset on the server is #%d.'
1539 % (local_patchset, latest_patchset))
1540 print('Uploading will still work, but if you\'ve uploaded to this '
1541 'issue from another machine or branch the patch you\'re '
1542 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001543 ask_for_data('About to upload; enter to confirm.')
1544
1545 print_stats(options.similarity, options.find_copies, git_diff_args)
1546 ret = self.CMDUploadChange(options, git_diff_args, change)
1547 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001548 if options.use_commit_queue:
1549 self.SetCQState(_CQState.COMMIT)
1550 elif options.cq_dry_run:
1551 self.SetCQState(_CQState.DRY_RUN)
1552
tandrii5d48c322016-08-18 16:19:37 -07001553 _git_set_branch_config_value('last-upload-hash',
1554 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001555 # Run post upload hooks, if specified.
1556 if settings.GetRunPostUploadHook():
1557 presubmit_support.DoPostUploadExecuter(
1558 change,
1559 self,
1560 settings.GetRoot(),
1561 options.verbose,
1562 sys.stdout)
1563
1564 # Upload all dependencies if specified.
1565 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001566 print()
1567 print('--dependencies has been specified.')
1568 print('All dependent local branches will be re-uploaded.')
1569 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570 # Remove the dependencies flag from args so that we do not end up in a
1571 # loop.
1572 orig_args.remove('--dependencies')
1573 ret = upload_branch_deps(self, orig_args)
1574 return ret
1575
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001576 def SetCQState(self, new_state):
1577 """Update the CQ state for latest patchset.
1578
1579 Issue must have been already uploaded and known.
1580 """
1581 assert new_state in _CQState.ALL_STATES
1582 assert self.GetIssue()
1583 return self._codereview_impl.SetCQState(new_state)
1584
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001585 # Forward methods to codereview specific implementation.
1586
1587 def CloseIssue(self):
1588 return self._codereview_impl.CloseIssue()
1589
1590 def GetStatus(self):
1591 return self._codereview_impl.GetStatus()
1592
1593 def GetCodereviewServer(self):
1594 return self._codereview_impl.GetCodereviewServer()
1595
tandriide281ae2016-10-12 06:02:30 -07001596 def GetIssueOwner(self):
1597 """Get owner from codereview, which may differ from this checkout."""
1598 return self._codereview_impl.GetIssueOwner()
1599
1600 def GetIssueProject(self):
1601 """Get project from codereview, which may differ from what this
1602 checkout's codereview.settings or gerrit project URL say.
1603 """
1604 return self._codereview_impl.GetIssueProject()
1605
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001606 def GetApprovingReviewers(self):
1607 return self._codereview_impl.GetApprovingReviewers()
1608
1609 def GetMostRecentPatchset(self):
1610 return self._codereview_impl.GetMostRecentPatchset()
1611
tandriide281ae2016-10-12 06:02:30 -07001612 def CannotTriggerTryJobReason(self):
1613 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1614 return self._codereview_impl.CannotTriggerTryJobReason()
1615
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001616 def __getattr__(self, attr):
1617 # This is because lots of untested code accesses Rietveld-specific stuff
1618 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001619 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001620 # Note that child method defines __getattr__ as well, and forwards it here,
1621 # because _RietveldChangelistImpl is not cleaned up yet, and given
1622 # deprecation of Rietveld, it should probably be just removed.
1623 # Until that time, avoid infinite recursion by bypassing __getattr__
1624 # of implementation class.
1625 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001626
1627
1628class _ChangelistCodereviewBase(object):
1629 """Abstract base class encapsulating codereview specifics of a changelist."""
1630 def __init__(self, changelist):
1631 self._changelist = changelist # instance of Changelist
1632
1633 def __getattr__(self, attr):
1634 # Forward methods to changelist.
1635 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1636 # _RietveldChangelistImpl to avoid this hack?
1637 return getattr(self._changelist, attr)
1638
1639 def GetStatus(self):
1640 """Apply a rough heuristic to give a simple summary of an issue's review
1641 or CQ status, assuming adherence to a common workflow.
1642
1643 Returns None if no issue for this branch, or specific string keywords.
1644 """
1645 raise NotImplementedError()
1646
1647 def GetCodereviewServer(self):
1648 """Returns server URL without end slash, like "https://codereview.com"."""
1649 raise NotImplementedError()
1650
1651 def FetchDescription(self):
1652 """Fetches and returns description from the codereview server."""
1653 raise NotImplementedError()
1654
tandrii5d48c322016-08-18 16:19:37 -07001655 @classmethod
1656 def IssueConfigKey(cls):
1657 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001658 raise NotImplementedError()
1659
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001660 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001661 def PatchsetConfigKey(cls):
1662 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001663 raise NotImplementedError()
1664
tandrii5d48c322016-08-18 16:19:37 -07001665 @classmethod
1666 def CodereviewServerConfigKey(cls):
1667 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668 raise NotImplementedError()
1669
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001670 def _PostUnsetIssueProperties(self):
1671 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001672 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001673
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 def GetRieveldObjForPresubmit(self):
1675 # This is an unfortunate Rietveld-embeddedness in presubmit.
1676 # For non-Rietveld codereviews, this probably should return a dummy object.
1677 raise NotImplementedError()
1678
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001679 def GetGerritObjForPresubmit(self):
1680 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1681 return None
1682
dsansomee2d6fd92016-09-08 00:10:47 -07001683 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001684 """Update the description on codereview site."""
1685 raise NotImplementedError()
1686
1687 def CloseIssue(self):
1688 """Closes the issue."""
1689 raise NotImplementedError()
1690
1691 def GetApprovingReviewers(self):
1692 """Returns a list of reviewers approving the change.
1693
1694 Note: not necessarily committers.
1695 """
1696 raise NotImplementedError()
1697
1698 def GetMostRecentPatchset(self):
1699 """Returns the most recent patchset number from the codereview site."""
1700 raise NotImplementedError()
1701
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001702 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1703 directory):
1704 """Fetches and applies the issue.
1705
1706 Arguments:
1707 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1708 reject: if True, reject the failed patch instead of switching to 3-way
1709 merge. Rietveld only.
1710 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1711 only.
1712 directory: switch to directory before applying the patch. Rietveld only.
1713 """
1714 raise NotImplementedError()
1715
1716 @staticmethod
1717 def ParseIssueURL(parsed_url):
1718 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1719 failed."""
1720 raise NotImplementedError()
1721
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001722 def EnsureAuthenticated(self, force):
1723 """Best effort check that user is authenticated with codereview server.
1724
1725 Arguments:
1726 force: whether to skip confirmation questions.
1727 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001728 raise NotImplementedError()
1729
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001730 def CMDUploadChange(self, options, args, change):
1731 """Uploads a change to codereview."""
1732 raise NotImplementedError()
1733
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001734 def SetCQState(self, new_state):
1735 """Update the CQ state for latest patchset.
1736
1737 Issue must have been already uploaded and known.
1738 """
1739 raise NotImplementedError()
1740
tandriie113dfd2016-10-11 10:20:12 -07001741 def CannotTriggerTryJobReason(self):
1742 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1743 raise NotImplementedError()
1744
tandriide281ae2016-10-12 06:02:30 -07001745 def GetIssueOwner(self):
1746 raise NotImplementedError()
1747
1748 def GetIssueProject(self):
1749 raise NotImplementedError()
1750
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001751
1752class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1753 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1754 super(_RietveldChangelistImpl, self).__init__(changelist)
1755 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001756 if not rietveld_server:
1757 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758
1759 self._rietveld_server = rietveld_server
1760 self._auth_config = auth_config
1761 self._props = None
1762 self._rpc_server = None
1763
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 def GetCodereviewServer(self):
1765 if not self._rietveld_server:
1766 # If we're on a branch then get the server potentially associated
1767 # with that branch.
1768 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001769 self._rietveld_server = gclient_utils.UpgradeToHttps(
1770 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 if not self._rietveld_server:
1772 self._rietveld_server = settings.GetDefaultServerUrl()
1773 return self._rietveld_server
1774
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001775 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001776 """Best effort check that user is authenticated with Rietveld server."""
1777 if self._auth_config.use_oauth2:
1778 authenticator = auth.get_authenticator_for_host(
1779 self.GetCodereviewServer(), self._auth_config)
1780 if not authenticator.has_cached_credentials():
1781 raise auth.LoginRequiredError(self.GetCodereviewServer())
1782
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 def FetchDescription(self):
1784 issue = self.GetIssue()
1785 assert issue
1786 try:
1787 return self.RpcServer().get_description(issue).strip()
1788 except urllib2.HTTPError as e:
1789 if e.code == 404:
1790 DieWithError(
1791 ('\nWhile fetching the description for issue %d, received a '
1792 '404 (not found)\n'
1793 'error. It is likely that you deleted this '
1794 'issue on the server. If this is the\n'
1795 'case, please run\n\n'
1796 ' git cl issue 0\n\n'
1797 'to clear the association with the deleted issue. Then run '
1798 'this command again.') % issue)
1799 else:
1800 DieWithError(
1801 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1802 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001803 print('Warning: Failed to retrieve CL description due to network '
1804 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 return ''
1806
1807 def GetMostRecentPatchset(self):
1808 return self.GetIssueProperties()['patchsets'][-1]
1809
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 def GetIssueProperties(self):
1811 if self._props is None:
1812 issue = self.GetIssue()
1813 if not issue:
1814 self._props = {}
1815 else:
1816 self._props = self.RpcServer().get_issue_properties(issue, True)
1817 return self._props
1818
tandriie113dfd2016-10-11 10:20:12 -07001819 def CannotTriggerTryJobReason(self):
1820 props = self.GetIssueProperties()
1821 if not props:
1822 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1823 if props.get('closed'):
1824 return 'CL %s is closed' % self.GetIssue()
1825 if props.get('private'):
1826 return 'CL %s is private' % self.GetIssue()
1827 return None
1828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829 def GetApprovingReviewers(self):
1830 return get_approving_reviewers(self.GetIssueProperties())
1831
tandriide281ae2016-10-12 06:02:30 -07001832 def GetIssueOwner(self):
1833 return (self.GetIssueProperties() or {}).get('owner_email')
1834
1835 def GetIssueProject(self):
1836 return (self.GetIssueProperties() or {}).get('project')
1837
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838 def AddComment(self, message):
1839 return self.RpcServer().add_comment(self.GetIssue(), message)
1840
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001841 def GetStatus(self):
1842 """Apply a rough heuristic to give a simple summary of an issue's review
1843 or CQ status, assuming adherence to a common workflow.
1844
1845 Returns None if no issue for this branch, or one of the following keywords:
1846 * 'error' - error from review tool (including deleted issues)
1847 * 'unsent' - not sent for review
1848 * 'waiting' - waiting for review
1849 * 'reply' - waiting for owner to reply to review
1850 * 'lgtm' - LGTM from at least one approved reviewer
1851 * 'commit' - in the commit queue
1852 * 'closed' - closed
1853 """
1854 if not self.GetIssue():
1855 return None
1856
1857 try:
1858 props = self.GetIssueProperties()
1859 except urllib2.HTTPError:
1860 return 'error'
1861
1862 if props.get('closed'):
1863 # Issue is closed.
1864 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001865 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001866 # Issue is in the commit queue.
1867 return 'commit'
1868
1869 try:
1870 reviewers = self.GetApprovingReviewers()
1871 except urllib2.HTTPError:
1872 return 'error'
1873
1874 if reviewers:
1875 # Was LGTM'ed.
1876 return 'lgtm'
1877
1878 messages = props.get('messages') or []
1879
tandrii9d2c7a32016-06-22 03:42:45 -07001880 # Skip CQ messages that don't require owner's action.
1881 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1882 if 'Dry run:' in messages[-1]['text']:
1883 messages.pop()
1884 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1885 # This message always follows prior messages from CQ,
1886 # so skip this too.
1887 messages.pop()
1888 else:
1889 # This is probably a CQ messages warranting user attention.
1890 break
1891
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001892 if not messages:
1893 # No message was sent.
1894 return 'unsent'
1895 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001896 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001897 return 'reply'
1898 return 'waiting'
1899
dsansomee2d6fd92016-09-08 00:10:47 -07001900 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001901 return self.RpcServer().update_description(
1902 self.GetIssue(), self.description)
1903
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001904 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001905 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001907 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001908 return self.SetFlags({flag: value})
1909
1910 def SetFlags(self, flags):
1911 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001912 """
phajdan.jr68598232016-08-10 03:28:28 -07001913 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001914 try:
tandrii4b233bd2016-07-06 03:50:29 -07001915 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001916 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001917 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001918 if e.code == 404:
1919 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1920 if e.code == 403:
1921 DieWithError(
1922 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001923 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001924 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001926 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 """Returns an upload.RpcServer() to access this review's rietveld instance.
1928 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001929 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001930 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001932 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001933 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001935 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001936 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001937 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938
tandrii5d48c322016-08-18 16:19:37 -07001939 @classmethod
1940 def PatchsetConfigKey(cls):
1941 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942
tandrii5d48c322016-08-18 16:19:37 -07001943 @classmethod
1944 def CodereviewServerConfigKey(cls):
1945 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001946
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 def GetRieveldObjForPresubmit(self):
1948 return self.RpcServer()
1949
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001950 def SetCQState(self, new_state):
1951 props = self.GetIssueProperties()
1952 if props.get('private'):
1953 DieWithError('Cannot set-commit on private issue')
1954
1955 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001956 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001957 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001958 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001959 else:
tandrii4b233bd2016-07-06 03:50:29 -07001960 assert new_state == _CQState.DRY_RUN
1961 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001962
1963
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001964 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1965 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001966 # PatchIssue should never be called with a dirty tree. It is up to the
1967 # caller to check this, but just in case we assert here since the
1968 # consequences of the caller not checking this could be dire.
1969 assert(not git_common.is_dirty_git_tree('apply'))
1970 assert(parsed_issue_arg.valid)
1971 self._changelist.issue = parsed_issue_arg.issue
1972 if parsed_issue_arg.hostname:
1973 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1974
skobes6468b902016-10-24 08:45:10 -07001975 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1976 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
1977 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001978 try:
skobes6468b902016-10-24 08:45:10 -07001979 scm_obj.apply_patch(patchset_object)
1980 except Exception as e:
1981 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001982 return 1
1983
1984 # If we had an issue, commit the current state and register the issue.
1985 if not nocommit:
1986 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1987 'patch from issue %(i)s at patchset '
1988 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1989 % {'i': self.GetIssue(), 'p': patchset})])
1990 self.SetIssue(self.GetIssue())
1991 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001992 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001993 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001994 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001995 return 0
1996
1997 @staticmethod
1998 def ParseIssueURL(parsed_url):
1999 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2000 return None
wychen3c1c1722016-08-04 11:46:36 -07002001 # Rietveld patch: https://domain/<number>/#ps<patchset>
2002 match = re.match(r'/(\d+)/$', parsed_url.path)
2003 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2004 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002005 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002006 issue=int(match.group(1)),
2007 patchset=int(match2.group(1)),
2008 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002009 # Typical url: https://domain/<issue_number>[/[other]]
2010 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2011 if match:
skobes6468b902016-10-24 08:45:10 -07002012 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002013 issue=int(match.group(1)),
2014 hostname=parsed_url.netloc)
2015 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2016 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2017 if match:
skobes6468b902016-10-24 08:45:10 -07002018 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002019 issue=int(match.group(1)),
2020 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002021 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002022 return None
2023
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002024 def CMDUploadChange(self, options, args, change):
2025 """Upload the patch to Rietveld."""
2026 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2027 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002028 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2029 if options.emulate_svn_auto_props:
2030 upload_args.append('--emulate_svn_auto_props')
2031
2032 change_desc = None
2033
2034 if options.email is not None:
2035 upload_args.extend(['--email', options.email])
2036
2037 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002038 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002039 upload_args.extend(['--title', options.title])
2040 if options.message:
2041 upload_args.extend(['--message', options.message])
2042 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002043 print('This branch is associated with issue %s. '
2044 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002045 else:
nodirca166002016-06-27 10:59:51 -07002046 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002047 upload_args.extend(['--title', options.title])
2048 message = (options.title or options.message or
2049 CreateDescriptionFromLog(args))
2050 change_desc = ChangeDescription(message)
2051 if options.reviewers or options.tbr_owners:
2052 change_desc.update_reviewers(options.reviewers,
2053 options.tbr_owners,
2054 change)
2055 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002056 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002057
2058 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002059 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002060 return 1
2061
2062 upload_args.extend(['--message', change_desc.description])
2063 if change_desc.get_reviewers():
2064 upload_args.append('--reviewers=%s' % ','.join(
2065 change_desc.get_reviewers()))
2066 if options.send_mail:
2067 if not change_desc.get_reviewers():
2068 DieWithError("Must specify reviewers to send email.")
2069 upload_args.append('--send_mail')
2070
2071 # We check this before applying rietveld.private assuming that in
2072 # rietveld.cc only addresses which we can send private CLs to are listed
2073 # if rietveld.private is set, and so we should ignore rietveld.cc only
2074 # when --private is specified explicitly on the command line.
2075 if options.private:
2076 logging.warn('rietveld.cc is ignored since private flag is specified. '
2077 'You need to review and add them manually if necessary.')
2078 cc = self.GetCCListWithoutDefault()
2079 else:
2080 cc = self.GetCCList()
2081 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002082 if change_desc.get_cced():
2083 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002084 if cc:
2085 upload_args.extend(['--cc', cc])
2086
2087 if options.private or settings.GetDefaultPrivateFlag() == "True":
2088 upload_args.append('--private')
2089
2090 upload_args.extend(['--git_similarity', str(options.similarity)])
2091 if not options.find_copies:
2092 upload_args.extend(['--git_no_find_copies'])
2093
2094 # Include the upstream repo's URL in the change -- this is useful for
2095 # projects that have their source spread across multiple repos.
2096 remote_url = self.GetGitBaseUrlFromConfig()
2097 if not remote_url:
2098 if settings.GetIsGitSvn():
2099 remote_url = self.GetGitSvnRemoteUrl()
2100 else:
2101 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2102 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2103 self.GetUpstreamBranch().split('/')[-1])
2104 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002105 remote, remote_branch = self.GetRemoteBranch()
2106 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2107 settings.GetPendingRefPrefix())
2108 if target_ref:
2109 upload_args.extend(['--target_ref', target_ref])
2110
2111 # Look for dependent patchsets. See crbug.com/480453 for more details.
2112 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2113 upstream_branch = ShortBranchName(upstream_branch)
2114 if remote is '.':
2115 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002116 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002117 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002118 print()
2119 print('Skipping dependency patchset upload because git config '
2120 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2121 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122 else:
2123 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002124 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002125 auth_config=auth_config)
2126 branch_cl_issue_url = branch_cl.GetIssueURL()
2127 branch_cl_issue = branch_cl.GetIssue()
2128 branch_cl_patchset = branch_cl.GetPatchset()
2129 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2130 upload_args.extend(
2131 ['--depends_on_patchset', '%s:%s' % (
2132 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002133 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002134 '\n'
2135 'The current branch (%s) is tracking a local branch (%s) with '
2136 'an associated CL.\n'
2137 'Adding %s/#ps%s as a dependency patchset.\n'
2138 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2139 branch_cl_patchset))
2140
2141 project = settings.GetProject()
2142 if project:
2143 upload_args.extend(['--project', project])
2144
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002145 try:
2146 upload_args = ['upload'] + upload_args + args
2147 logging.info('upload.RealMain(%s)', upload_args)
2148 issue, patchset = upload.RealMain(upload_args)
2149 issue = int(issue)
2150 patchset = int(patchset)
2151 except KeyboardInterrupt:
2152 sys.exit(1)
2153 except:
2154 # If we got an exception after the user typed a description for their
2155 # change, back up the description before re-raising.
2156 if change_desc:
2157 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2158 print('\nGot exception while uploading -- saving description to %s\n' %
2159 backup_path)
2160 backup_file = open(backup_path, 'w')
2161 backup_file.write(change_desc.description)
2162 backup_file.close()
2163 raise
2164
2165 if not self.GetIssue():
2166 self.SetIssue(issue)
2167 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002168 return 0
2169
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170
2171class _GerritChangelistImpl(_ChangelistCodereviewBase):
2172 def __init__(self, changelist, auth_config=None):
2173 # auth_config is Rietveld thing, kept here to preserve interface only.
2174 super(_GerritChangelistImpl, self).__init__(changelist)
2175 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002176 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002177 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002178 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002179
2180 def _GetGerritHost(self):
2181 # Lazy load of configs.
2182 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002183 if self._gerrit_host and '.' not in self._gerrit_host:
2184 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2185 # This happens for internal stuff http://crbug.com/614312.
2186 parsed = urlparse.urlparse(self.GetRemoteUrl())
2187 if parsed.scheme == 'sso':
2188 print('WARNING: using non https URLs for remote is likely broken\n'
2189 ' Your current remote is: %s' % self.GetRemoteUrl())
2190 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2191 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002192 return self._gerrit_host
2193
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002194 def _GetGitHost(self):
2195 """Returns git host to be used when uploading change to Gerrit."""
2196 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2197
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002198 def GetCodereviewServer(self):
2199 if not self._gerrit_server:
2200 # If we're on a branch then get the server potentially associated
2201 # with that branch.
2202 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002203 self._gerrit_server = self._GitGetBranchConfigValue(
2204 self.CodereviewServerConfigKey())
2205 if self._gerrit_server:
2206 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002207 if not self._gerrit_server:
2208 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2209 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002210 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002211 parts[0] = parts[0] + '-review'
2212 self._gerrit_host = '.'.join(parts)
2213 self._gerrit_server = 'https://%s' % self._gerrit_host
2214 return self._gerrit_server
2215
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002216 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002217 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002218 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002219
tandrii5d48c322016-08-18 16:19:37 -07002220 @classmethod
2221 def PatchsetConfigKey(cls):
2222 return 'gerritpatchset'
2223
2224 @classmethod
2225 def CodereviewServerConfigKey(cls):
2226 return 'gerritserver'
2227
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002228 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002229 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002230 if settings.GetGerritSkipEnsureAuthenticated():
2231 # For projects with unusual authentication schemes.
2232 # See http://crbug.com/603378.
2233 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002234 # Lazy-loader to identify Gerrit and Git hosts.
2235 if gerrit_util.GceAuthenticator.is_gce():
2236 return
2237 self.GetCodereviewServer()
2238 git_host = self._GetGitHost()
2239 assert self._gerrit_server and self._gerrit_host
2240 cookie_auth = gerrit_util.CookiesAuthenticator()
2241
2242 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2243 git_auth = cookie_auth.get_auth_header(git_host)
2244 if gerrit_auth and git_auth:
2245 if gerrit_auth == git_auth:
2246 return
2247 print((
2248 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2249 ' Check your %s or %s file for credentials of hosts:\n'
2250 ' %s\n'
2251 ' %s\n'
2252 ' %s') %
2253 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2254 git_host, self._gerrit_host,
2255 cookie_auth.get_new_password_message(git_host)))
2256 if not force:
2257 ask_for_data('If you know what you are doing, press Enter to continue, '
2258 'Ctrl+C to abort.')
2259 return
2260 else:
2261 missing = (
2262 [] if gerrit_auth else [self._gerrit_host] +
2263 [] if git_auth else [git_host])
2264 DieWithError('Credentials for the following hosts are required:\n'
2265 ' %s\n'
2266 'These are read from %s (or legacy %s)\n'
2267 '%s' % (
2268 '\n '.join(missing),
2269 cookie_auth.get_gitcookies_path(),
2270 cookie_auth.get_netrc_path(),
2271 cookie_auth.get_new_password_message(git_host)))
2272
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002273 def _PostUnsetIssueProperties(self):
2274 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002275 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002276
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002277 def GetRieveldObjForPresubmit(self):
2278 class ThisIsNotRietveldIssue(object):
2279 def __nonzero__(self):
2280 # This is a hack to make presubmit_support think that rietveld is not
2281 # defined, yet still ensure that calls directly result in a decent
2282 # exception message below.
2283 return False
2284
2285 def __getattr__(self, attr):
2286 print(
2287 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2288 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2289 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2290 'or use Rietveld for codereview.\n'
2291 'See also http://crbug.com/579160.' % attr)
2292 raise NotImplementedError()
2293 return ThisIsNotRietveldIssue()
2294
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002295 def GetGerritObjForPresubmit(self):
2296 return presubmit_support.GerritAccessor(self._GetGerritHost())
2297
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002298 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002299 """Apply a rough heuristic to give a simple summary of an issue's review
2300 or CQ status, assuming adherence to a common workflow.
2301
2302 Returns None if no issue for this branch, or one of the following keywords:
2303 * 'error' - error from review tool (including deleted issues)
2304 * 'unsent' - no reviewers added
2305 * 'waiting' - waiting for review
2306 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002307 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2308 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002309 * 'commit' - in the commit queue
2310 * 'closed' - abandoned
2311 """
2312 if not self.GetIssue():
2313 return None
2314
2315 try:
2316 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002317 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002318 return 'error'
2319
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002320 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002321 return 'closed'
2322
2323 cq_label = data['labels'].get('Commit-Queue', {})
2324 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002325 votes = cq_label.get('all', [])
2326 highest_vote = 0
2327 for v in votes:
2328 highest_vote = max(highest_vote, v.get('value', 0))
2329 vote_value = str(highest_vote)
2330 if vote_value != '0':
2331 # Add a '+' if the value is not 0 to match the values in the label.
2332 # The cq_label does not have negatives.
2333 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002334 vote_text = cq_label.get('values', {}).get(vote_value, '')
2335 if vote_text.lower() == 'commit':
2336 return 'commit'
2337
2338 lgtm_label = data['labels'].get('Code-Review', {})
2339 if lgtm_label:
2340 if 'rejected' in lgtm_label:
2341 return 'not lgtm'
2342 if 'approved' in lgtm_label:
2343 return 'lgtm'
2344
2345 if not data.get('reviewers', {}).get('REVIEWER', []):
2346 return 'unsent'
2347
2348 messages = data.get('messages', [])
2349 if messages:
2350 owner = data['owner'].get('_account_id')
2351 last_message_author = messages[-1].get('author', {}).get('_account_id')
2352 if owner != last_message_author:
2353 # Some reply from non-owner.
2354 return 'reply'
2355
2356 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002357
2358 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002359 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002360 return data['revisions'][data['current_revision']]['_number']
2361
2362 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002363 data = self._GetChangeDetail(['CURRENT_REVISION'])
2364 current_rev = data['current_revision']
2365 url = data['revisions'][current_rev]['fetch']['http']['url']
2366 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002367
dsansomee2d6fd92016-09-08 00:10:47 -07002368 def UpdateDescriptionRemote(self, description, force=False):
2369 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2370 if not force:
2371 ask_for_data(
2372 'The description cannot be modified while the issue has a pending '
2373 'unpublished edit. Either publish the edit in the Gerrit web UI '
2374 'or delete it.\n\n'
2375 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2376
2377 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2378 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002379 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2380 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381
2382 def CloseIssue(self):
2383 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2384
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002385 def GetApprovingReviewers(self):
2386 """Returns a list of reviewers approving the change.
2387
2388 Note: not necessarily committers.
2389 """
2390 raise NotImplementedError()
2391
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002392 def SubmitIssue(self, wait_for_merge=True):
2393 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2394 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002396 def _GetChangeDetail(self, options=None, issue=None):
2397 options = options or []
2398 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002399 assert issue, 'issue is required to query Gerrit'
2400 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002401 options)
tandriic2405f52016-10-10 08:13:15 -07002402 if not data:
2403 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2404 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002405
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002406 def CMDLand(self, force, bypass_hooks, verbose):
2407 if git_common.is_dirty_git_tree('land'):
2408 return 1
tandriid60367b2016-06-22 05:25:12 -07002409 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2410 if u'Commit-Queue' in detail.get('labels', {}):
2411 if not force:
2412 ask_for_data('\nIt seems this repository has a Commit Queue, '
2413 'which can test and land changes for you. '
2414 'Are you sure you wish to bypass it?\n'
2415 'Press Enter to continue, Ctrl+C to abort.')
2416
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002417 differs = True
tandriic4344b52016-08-29 06:04:54 -07002418 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002419 # Note: git diff outputs nothing if there is no diff.
2420 if not last_upload or RunGit(['diff', last_upload]).strip():
2421 print('WARNING: some changes from local branch haven\'t been uploaded')
2422 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002423 if detail['current_revision'] == last_upload:
2424 differs = False
2425 else:
2426 print('WARNING: local branch contents differ from latest uploaded '
2427 'patchset')
2428 if differs:
2429 if not force:
2430 ask_for_data(
2431 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2432 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2433 elif not bypass_hooks:
2434 hook_results = self.RunHook(
2435 committing=True,
2436 may_prompt=not force,
2437 verbose=verbose,
2438 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2439 if not hook_results.should_continue():
2440 return 1
2441
2442 self.SubmitIssue(wait_for_merge=True)
2443 print('Issue %s has been submitted.' % self.GetIssueURL())
2444 return 0
2445
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002446 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2447 directory):
2448 assert not reject
2449 assert not nocommit
2450 assert not directory
2451 assert parsed_issue_arg.valid
2452
2453 self._changelist.issue = parsed_issue_arg.issue
2454
2455 if parsed_issue_arg.hostname:
2456 self._gerrit_host = parsed_issue_arg.hostname
2457 self._gerrit_server = 'https://%s' % self._gerrit_host
2458
tandriic2405f52016-10-10 08:13:15 -07002459 try:
2460 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2461 except GerritIssueNotExists as e:
2462 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002463
2464 if not parsed_issue_arg.patchset:
2465 # Use current revision by default.
2466 revision_info = detail['revisions'][detail['current_revision']]
2467 patchset = int(revision_info['_number'])
2468 else:
2469 patchset = parsed_issue_arg.patchset
2470 for revision_info in detail['revisions'].itervalues():
2471 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2472 break
2473 else:
2474 DieWithError('Couldn\'t find patchset %i in issue %i' %
2475 (parsed_issue_arg.patchset, self.GetIssue()))
2476
2477 fetch_info = revision_info['fetch']['http']
2478 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2479 RunGit(['cherry-pick', 'FETCH_HEAD'])
2480 self.SetIssue(self.GetIssue())
2481 self.SetPatchset(patchset)
2482 print('Committed patch for issue %i pathset %i locally' %
2483 (self.GetIssue(), self.GetPatchset()))
2484 return 0
2485
2486 @staticmethod
2487 def ParseIssueURL(parsed_url):
2488 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2489 return None
2490 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2491 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2492 # Short urls like https://domain/<issue_number> can be used, but don't allow
2493 # specifying the patchset (you'd 404), but we allow that here.
2494 if parsed_url.path == '/':
2495 part = parsed_url.fragment
2496 else:
2497 part = parsed_url.path
2498 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2499 if match:
2500 return _ParsedIssueNumberArgument(
2501 issue=int(match.group(2)),
2502 patchset=int(match.group(4)) if match.group(4) else None,
2503 hostname=parsed_url.netloc)
2504 return None
2505
tandrii16e0b4e2016-06-07 10:34:28 -07002506 def _GerritCommitMsgHookCheck(self, offer_removal):
2507 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2508 if not os.path.exists(hook):
2509 return
2510 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2511 # custom developer made one.
2512 data = gclient_utils.FileRead(hook)
2513 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2514 return
2515 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002516 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002517 'and may interfere with it in subtle ways.\n'
2518 'We recommend you remove the commit-msg hook.')
2519 if offer_removal:
2520 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2521 if reply.lower().startswith('y'):
2522 gclient_utils.rm_file_or_tree(hook)
2523 print('Gerrit commit-msg hook removed.')
2524 else:
2525 print('OK, will keep Gerrit commit-msg hook in place.')
2526
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002527 def CMDUploadChange(self, options, args, change):
2528 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002529 if options.squash and options.no_squash:
2530 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002531
2532 if not options.squash and not options.no_squash:
2533 # Load default for user, repo, squash=true, in this order.
2534 options.squash = settings.GetSquashGerritUploads()
2535 elif options.no_squash:
2536 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002537
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002538 # We assume the remote called "origin" is the one we want.
2539 # It is probably not worthwhile to support different workflows.
2540 gerrit_remote = 'origin'
2541
2542 remote, remote_branch = self.GetRemoteBranch()
2543 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2544 pending_prefix='')
2545
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002546 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002547 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002548 if self.GetIssue():
2549 # Try to get the message from a previous upload.
2550 message = self.GetDescription()
2551 if not message:
2552 DieWithError(
2553 'failed to fetch description from current Gerrit issue %d\n'
2554 '%s' % (self.GetIssue(), self.GetIssueURL()))
2555 change_id = self._GetChangeDetail()['change_id']
2556 while True:
2557 footer_change_ids = git_footers.get_footer_change_id(message)
2558 if footer_change_ids == [change_id]:
2559 break
2560 if not footer_change_ids:
2561 message = git_footers.add_footer_change_id(message, change_id)
2562 print('WARNING: appended missing Change-Id to issue description')
2563 continue
2564 # There is already a valid footer but with different or several ids.
2565 # Doing this automatically is non-trivial as we don't want to lose
2566 # existing other footers, yet we want to append just 1 desired
2567 # Change-Id. Thus, just create a new footer, but let user verify the
2568 # new description.
2569 message = '%s\n\nChange-Id: %s' % (message, change_id)
2570 print(
2571 'WARNING: issue %s has Change-Id footer(s):\n'
2572 ' %s\n'
2573 'but issue has Change-Id %s, according to Gerrit.\n'
2574 'Please, check the proposed correction to the description, '
2575 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2576 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2577 change_id))
2578 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2579 if not options.force:
2580 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002581 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002582 message = change_desc.description
2583 if not message:
2584 DieWithError("Description is empty. Aborting...")
2585 # Continue the while loop.
2586 # Sanity check of this code - we should end up with proper message
2587 # footer.
2588 assert [change_id] == git_footers.get_footer_change_id(message)
2589 change_desc = ChangeDescription(message)
2590 else:
2591 change_desc = ChangeDescription(
2592 options.message or CreateDescriptionFromLog(args))
2593 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002594 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002595 if not change_desc.description:
2596 DieWithError("Description is empty. Aborting...")
2597 message = change_desc.description
2598 change_ids = git_footers.get_footer_change_id(message)
2599 if len(change_ids) > 1:
2600 DieWithError('too many Change-Id footers, at most 1 allowed.')
2601 if not change_ids:
2602 # Generate the Change-Id automatically.
2603 message = git_footers.add_footer_change_id(
2604 message, GenerateGerritChangeId(message))
2605 change_desc.set_description(message)
2606 change_ids = git_footers.get_footer_change_id(message)
2607 assert len(change_ids) == 1
2608 change_id = change_ids[0]
2609
2610 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2611 if remote is '.':
2612 # If our upstream branch is local, we base our squashed commit on its
2613 # squashed version.
2614 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2615 # Check the squashed hash of the parent.
2616 parent = RunGit(['config',
2617 'branch.%s.gerritsquashhash' % upstream_branch_name],
2618 error_ok=True).strip()
2619 # Verify that the upstream branch has been uploaded too, otherwise
2620 # Gerrit will create additional CLs when uploading.
2621 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2622 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002623 DieWithError(
2624 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002625 'Note: maybe you\'ve uploaded it with --no-squash. '
2626 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002627 ' git cl upload --squash\n' % upstream_branch_name)
2628 else:
2629 parent = self.GetCommonAncestorWithUpstream()
2630
2631 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2632 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2633 '-m', message]).strip()
2634 else:
2635 change_desc = ChangeDescription(
2636 options.message or CreateDescriptionFromLog(args))
2637 if not change_desc.description:
2638 DieWithError("Description is empty. Aborting...")
2639
2640 if not git_footers.get_footer_change_id(change_desc.description):
2641 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002642 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2643 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 ref_to_push = 'HEAD'
2645 parent = '%s/%s' % (gerrit_remote, branch)
2646 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2647
2648 assert change_desc
2649 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2650 ref_to_push)]).splitlines()
2651 if len(commits) > 1:
2652 print('WARNING: This will upload %d commits. Run the following command '
2653 'to see which commits will be uploaded: ' % len(commits))
2654 print('git log %s..%s' % (parent, ref_to_push))
2655 print('You can also use `git squash-branch` to squash these into a '
2656 'single commit.')
2657 ask_for_data('About to upload; enter to confirm.')
2658
2659 if options.reviewers or options.tbr_owners:
2660 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2661 change)
2662
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002663 # Extra options that can be specified at push time. Doc:
2664 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2665 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002666 if change_desc.get_reviewers(tbr_only=True):
2667 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2668 refspec_opts.append('l=Code-Review+1')
2669
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002670 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002671 if not re.match(r'^[\w ]+$', options.title):
2672 options.title = re.sub(r'[^\w ]', '', options.title)
2673 print('WARNING: Patchset title may only contain alphanumeric chars '
2674 'and spaces. Cleaned up title:\n%s' % options.title)
2675 if not options.force:
2676 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002677 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2678 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002679 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2680
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002681 if options.send_mail:
2682 if not change_desc.get_reviewers():
2683 DieWithError('Must specify reviewers to send email.')
2684 refspec_opts.append('notify=ALL')
2685 else:
2686 refspec_opts.append('notify=NONE')
2687
tandrii99a72f22016-08-17 14:33:24 -07002688 reviewers = change_desc.get_reviewers()
2689 if reviewers:
2690 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002691
agablec6787972016-09-09 16:13:34 -07002692 if options.private:
2693 refspec_opts.append('draft')
2694
rmistry9eadede2016-09-19 11:22:43 -07002695 if options.topic:
2696 # Documentation on Gerrit topics is here:
2697 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2698 refspec_opts.append('topic=%s' % options.topic)
2699
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002700 refspec_suffix = ''
2701 if refspec_opts:
2702 refspec_suffix = '%' + ','.join(refspec_opts)
2703 assert ' ' not in refspec_suffix, (
2704 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002705 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002706
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002707 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002708 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 print_stdout=True,
2710 # Flush after every line: useful for seeing progress when running as
2711 # recipe.
2712 filter_fn=lambda _: sys.stdout.flush())
2713
2714 if options.squash:
2715 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2716 change_numbers = [m.group(1)
2717 for m in map(regex.match, push_stdout.splitlines())
2718 if m]
2719 if len(change_numbers) != 1:
2720 DieWithError(
2721 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2722 'Change-Id: %s') % (len(change_numbers), change_id))
2723 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002724 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002725
2726 # Add cc's from the CC_LIST and --cc flag (if any).
2727 cc = self.GetCCList().split(',')
2728 if options.cc:
2729 cc.extend(options.cc)
2730 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002731 if change_desc.get_cced():
2732 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002733 if cc:
2734 gerrit_util.AddReviewers(
2735 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2736
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 return 0
2738
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002739 def _AddChangeIdToCommitMessage(self, options, args):
2740 """Re-commits using the current message, assumes the commit hook is in
2741 place.
2742 """
2743 log_desc = options.message or CreateDescriptionFromLog(args)
2744 git_command = ['commit', '--amend', '-m', log_desc]
2745 RunGit(git_command)
2746 new_log_desc = CreateDescriptionFromLog(args)
2747 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002748 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002749 return new_log_desc
2750 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002751 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002752
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002753 def SetCQState(self, new_state):
2754 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002755 vote_map = {
2756 _CQState.NONE: 0,
2757 _CQState.DRY_RUN: 1,
2758 _CQState.COMMIT : 2,
2759 }
2760 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2761 labels={'Commit-Queue': vote_map[new_state]})
2762
tandriie113dfd2016-10-11 10:20:12 -07002763 def CannotTriggerTryJobReason(self):
2764 # TODO(tandrii): implement for Gerrit.
2765 raise NotImplementedError()
2766
tandriide281ae2016-10-12 06:02:30 -07002767 def GetIssueOwner(self):
2768 # TODO(tandrii): implement for Gerrit.
2769 raise NotImplementedError()
2770
2771 def GetIssueProject(self):
2772 # TODO(tandrii): implement for Gerrit.
2773 raise NotImplementedError()
2774
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002775
2776_CODEREVIEW_IMPLEMENTATIONS = {
2777 'rietveld': _RietveldChangelistImpl,
2778 'gerrit': _GerritChangelistImpl,
2779}
2780
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002781
iannuccie53c9352016-08-17 14:40:40 -07002782def _add_codereview_issue_select_options(parser, extra=""):
2783 _add_codereview_select_options(parser)
2784
2785 text = ('Operate on this issue number instead of the current branch\'s '
2786 'implicit issue.')
2787 if extra:
2788 text += ' '+extra
2789 parser.add_option('-i', '--issue', type=int, help=text)
2790
2791
2792def _process_codereview_issue_select_options(parser, options):
2793 _process_codereview_select_options(parser, options)
2794 if options.issue is not None and not options.forced_codereview:
2795 parser.error('--issue must be specified with either --rietveld or --gerrit')
2796
2797
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002798def _add_codereview_select_options(parser):
2799 """Appends --gerrit and --rietveld options to force specific codereview."""
2800 parser.codereview_group = optparse.OptionGroup(
2801 parser, 'EXPERIMENTAL! Codereview override options')
2802 parser.add_option_group(parser.codereview_group)
2803 parser.codereview_group.add_option(
2804 '--gerrit', action='store_true',
2805 help='Force the use of Gerrit for codereview')
2806 parser.codereview_group.add_option(
2807 '--rietveld', action='store_true',
2808 help='Force the use of Rietveld for codereview')
2809
2810
2811def _process_codereview_select_options(parser, options):
2812 if options.gerrit and options.rietveld:
2813 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2814 options.forced_codereview = None
2815 if options.gerrit:
2816 options.forced_codereview = 'gerrit'
2817 elif options.rietveld:
2818 options.forced_codereview = 'rietveld'
2819
2820
tandriif9aefb72016-07-01 09:06:51 -07002821def _get_bug_line_values(default_project, bugs):
2822 """Given default_project and comma separated list of bugs, yields bug line
2823 values.
2824
2825 Each bug can be either:
2826 * a number, which is combined with default_project
2827 * string, which is left as is.
2828
2829 This function may produce more than one line, because bugdroid expects one
2830 project per line.
2831
2832 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2833 ['v8:123', 'chromium:789']
2834 """
2835 default_bugs = []
2836 others = []
2837 for bug in bugs.split(','):
2838 bug = bug.strip()
2839 if bug:
2840 try:
2841 default_bugs.append(int(bug))
2842 except ValueError:
2843 others.append(bug)
2844
2845 if default_bugs:
2846 default_bugs = ','.join(map(str, default_bugs))
2847 if default_project:
2848 yield '%s:%s' % (default_project, default_bugs)
2849 else:
2850 yield default_bugs
2851 for other in sorted(others):
2852 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2853 yield other
2854
2855
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002856class ChangeDescription(object):
2857 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002858 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002859 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002860 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002861
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002862 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002863 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002864
agable@chromium.org42c20792013-09-12 17:34:49 +00002865 @property # www.logilab.org/ticket/89786
2866 def description(self): # pylint: disable=E0202
2867 return '\n'.join(self._description_lines)
2868
2869 def set_description(self, desc):
2870 if isinstance(desc, basestring):
2871 lines = desc.splitlines()
2872 else:
2873 lines = [line.rstrip() for line in desc]
2874 while lines and not lines[0]:
2875 lines.pop(0)
2876 while lines and not lines[-1]:
2877 lines.pop(-1)
2878 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002879
piman@chromium.org336f9122014-09-04 02:16:55 +00002880 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002881 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002882 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002883 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002884 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002885 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002886
agable@chromium.org42c20792013-09-12 17:34:49 +00002887 # Get the set of R= and TBR= lines and remove them from the desciption.
2888 regexp = re.compile(self.R_LINE)
2889 matches = [regexp.match(line) for line in self._description_lines]
2890 new_desc = [l for i, l in enumerate(self._description_lines)
2891 if not matches[i]]
2892 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002893
agable@chromium.org42c20792013-09-12 17:34:49 +00002894 # Construct new unified R= and TBR= lines.
2895 r_names = []
2896 tbr_names = []
2897 for match in matches:
2898 if not match:
2899 continue
2900 people = cleanup_list([match.group(2).strip()])
2901 if match.group(1) == 'TBR':
2902 tbr_names.extend(people)
2903 else:
2904 r_names.extend(people)
2905 for name in r_names:
2906 if name not in reviewers:
2907 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002908 if add_owners_tbr:
2909 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002910 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002911 all_reviewers = set(tbr_names + reviewers)
2912 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2913 all_reviewers)
2914 tbr_names.extend(owners_db.reviewers_for(missing_files,
2915 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002916 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2917 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2918
2919 # Put the new lines in the description where the old first R= line was.
2920 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2921 if 0 <= line_loc < len(self._description_lines):
2922 if new_tbr_line:
2923 self._description_lines.insert(line_loc, new_tbr_line)
2924 if new_r_line:
2925 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002926 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002927 if new_r_line:
2928 self.append_footer(new_r_line)
2929 if new_tbr_line:
2930 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002931
tandriif9aefb72016-07-01 09:06:51 -07002932 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002933 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002934 self.set_description([
2935 '# Enter a description of the change.',
2936 '# This will be displayed on the codereview site.',
2937 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002938 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002939 '--------------------',
2940 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002941
agable@chromium.org42c20792013-09-12 17:34:49 +00002942 regexp = re.compile(self.BUG_LINE)
2943 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002944 prefix = settings.GetBugPrefix()
2945 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2946 for value in values:
2947 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2948 self.append_footer('BUG=%s' % value)
2949
agable@chromium.org42c20792013-09-12 17:34:49 +00002950 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002951 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002952 if not content:
2953 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002954 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002955
2956 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002957 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2958 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002959 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002960 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002961
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002962 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002963 """Adds a footer line to the description.
2964
2965 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2966 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2967 that Gerrit footers are always at the end.
2968 """
2969 parsed_footer_line = git_footers.parse_footer(line)
2970 if parsed_footer_line:
2971 # Line is a gerrit footer in the form: Footer-Key: any value.
2972 # Thus, must be appended observing Gerrit footer rules.
2973 self.set_description(
2974 git_footers.add_footer(self.description,
2975 key=parsed_footer_line[0],
2976 value=parsed_footer_line[1]))
2977 return
2978
2979 if not self._description_lines:
2980 self._description_lines.append(line)
2981 return
2982
2983 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2984 if gerrit_footers:
2985 # git_footers.split_footers ensures that there is an empty line before
2986 # actual (gerrit) footers, if any. We have to keep it that way.
2987 assert top_lines and top_lines[-1] == ''
2988 top_lines, separator = top_lines[:-1], top_lines[-1:]
2989 else:
2990 separator = [] # No need for separator if there are no gerrit_footers.
2991
2992 prev_line = top_lines[-1] if top_lines else ''
2993 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2994 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2995 top_lines.append('')
2996 top_lines.append(line)
2997 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002998
tandrii99a72f22016-08-17 14:33:24 -07002999 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003000 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003002 reviewers = [match.group(2).strip()
3003 for match in matches
3004 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003006
bradnelsond975b302016-10-23 12:20:23 -07003007 def get_cced(self):
3008 """Retrieves the list of reviewers."""
3009 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3010 cced = [match.group(2).strip() for match in matches if match]
3011 return cleanup_list(cced)
3012
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003013
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003014def get_approving_reviewers(props):
3015 """Retrieves the reviewers that approved a CL from the issue properties with
3016 messages.
3017
3018 Note that the list may contain reviewers that are not committer, thus are not
3019 considered by the CQ.
3020 """
3021 return sorted(
3022 set(
3023 message['sender']
3024 for message in props['messages']
3025 if message['approval'] and message['sender'] in props['reviewers']
3026 )
3027 )
3028
3029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003030def FindCodereviewSettingsFile(filename='codereview.settings'):
3031 """Finds the given file starting in the cwd and going up.
3032
3033 Only looks up to the top of the repository unless an
3034 'inherit-review-settings-ok' file exists in the root of the repository.
3035 """
3036 inherit_ok_file = 'inherit-review-settings-ok'
3037 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003038 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003039 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3040 root = '/'
3041 while True:
3042 if filename in os.listdir(cwd):
3043 if os.path.isfile(os.path.join(cwd, filename)):
3044 return open(os.path.join(cwd, filename))
3045 if cwd == root:
3046 break
3047 cwd = os.path.dirname(cwd)
3048
3049
3050def LoadCodereviewSettingsFromFile(fileobj):
3051 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003052 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003054 def SetProperty(name, setting, unset_error_ok=False):
3055 fullname = 'rietveld.' + name
3056 if setting in keyvals:
3057 RunGit(['config', fullname, keyvals[setting]])
3058 else:
3059 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3060
tandrii48df5812016-10-17 03:55:37 -07003061 if not keyvals.get('GERRIT_HOST', False):
3062 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003063 # Only server setting is required. Other settings can be absent.
3064 # In that case, we ignore errors raised during option deletion attempt.
3065 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003066 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3068 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003069 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003070 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003071 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3072 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003073 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003074 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003075 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003076 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003077 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3078 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003079
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003080 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003081 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003082
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003083 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003084 RunGit(['config', 'gerrit.squash-uploads',
3085 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003086
tandrii@chromium.org28253532016-04-14 13:46:56 +00003087 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003088 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003089 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003091 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3092 #should be of the form
3093 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3094 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3095 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3096 keyvals['ORIGIN_URL_CONFIG']])
3097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003099def urlretrieve(source, destination):
3100 """urllib is broken for SSL connections via a proxy therefore we
3101 can't use urllib.urlretrieve()."""
3102 with open(destination, 'w') as f:
3103 f.write(urllib2.urlopen(source).read())
3104
3105
ukai@chromium.org712d6102013-11-27 00:52:58 +00003106def hasSheBang(fname):
3107 """Checks fname is a #! script."""
3108 with open(fname) as f:
3109 return f.read(2).startswith('#!')
3110
3111
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003112# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3113def DownloadHooks(*args, **kwargs):
3114 pass
3115
3116
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003117def DownloadGerritHook(force):
3118 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003119
3120 Args:
3121 force: True to update hooks. False to install hooks if not present.
3122 """
3123 if not settings.GetIsGerrit():
3124 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003125 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003126 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3127 if not os.access(dst, os.X_OK):
3128 if os.path.exists(dst):
3129 if not force:
3130 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003131 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003132 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003133 if not hasSheBang(dst):
3134 DieWithError('Not a script: %s\n'
3135 'You need to download from\n%s\n'
3136 'into .git/hooks/commit-msg and '
3137 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003138 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3139 except Exception:
3140 if os.path.exists(dst):
3141 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003142 DieWithError('\nFailed to download hooks.\n'
3143 'You need to download from\n%s\n'
3144 'into .git/hooks/commit-msg and '
3145 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003146
3147
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003148
3149def GetRietveldCodereviewSettingsInteractively():
3150 """Prompt the user for settings."""
3151 server = settings.GetDefaultServerUrl(error_ok=True)
3152 prompt = 'Rietveld server (host[:port])'
3153 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3154 newserver = ask_for_data(prompt + ':')
3155 if not server and not newserver:
3156 newserver = DEFAULT_SERVER
3157 if newserver:
3158 newserver = gclient_utils.UpgradeToHttps(newserver)
3159 if newserver != server:
3160 RunGit(['config', 'rietveld.server', newserver])
3161
3162 def SetProperty(initial, caption, name, is_url):
3163 prompt = caption
3164 if initial:
3165 prompt += ' ("x" to clear) [%s]' % initial
3166 new_val = ask_for_data(prompt + ':')
3167 if new_val == 'x':
3168 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3169 elif new_val:
3170 if is_url:
3171 new_val = gclient_utils.UpgradeToHttps(new_val)
3172 if new_val != initial:
3173 RunGit(['config', 'rietveld.' + name, new_val])
3174
3175 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3176 SetProperty(settings.GetDefaultPrivateFlag(),
3177 'Private flag (rietveld only)', 'private', False)
3178 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3179 'tree-status-url', False)
3180 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3181 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3182 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3183 'run-post-upload-hook', False)
3184
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003185@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003187 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003188
tandrii5d0a0422016-09-14 06:24:35 -07003189 print('WARNING: git cl config works for Rietveld only')
3190 # TODO(tandrii): remove this once we switch to Gerrit.
3191 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003192 parser.add_option('--activate-update', action='store_true',
3193 help='activate auto-updating [rietveld] section in '
3194 '.git/config')
3195 parser.add_option('--deactivate-update', action='store_true',
3196 help='deactivate auto-updating [rietveld] section in '
3197 '.git/config')
3198 options, args = parser.parse_args(args)
3199
3200 if options.deactivate_update:
3201 RunGit(['config', 'rietveld.autoupdate', 'false'])
3202 return
3203
3204 if options.activate_update:
3205 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3206 return
3207
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003209 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003210 return 0
3211
3212 url = args[0]
3213 if not url.endswith('codereview.settings'):
3214 url = os.path.join(url, 'codereview.settings')
3215
3216 # Load code review settings and download hooks (if available).
3217 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3218 return 0
3219
3220
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003221def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003222 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003223 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3224 branch = ShortBranchName(branchref)
3225 _, args = parser.parse_args(args)
3226 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003227 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003228 return RunGit(['config', 'branch.%s.base-url' % branch],
3229 error_ok=False).strip()
3230 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003231 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003232 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3233 error_ok=False).strip()
3234
3235
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003236def color_for_status(status):
3237 """Maps a Changelist status to color, for CMDstatus and other tools."""
3238 return {
3239 'unsent': Fore.RED,
3240 'waiting': Fore.BLUE,
3241 'reply': Fore.YELLOW,
3242 'lgtm': Fore.GREEN,
3243 'commit': Fore.MAGENTA,
3244 'closed': Fore.CYAN,
3245 'error': Fore.WHITE,
3246 }.get(status, Fore.WHITE)
3247
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003248
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003249def get_cl_statuses(changes, fine_grained, max_processes=None):
3250 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003251
3252 If fine_grained is true, this will fetch CL statuses from the server.
3253 Otherwise, simply indicate if there's a matching url for the given branches.
3254
3255 If max_processes is specified, it is used as the maximum number of processes
3256 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3257 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003258
3259 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003260 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003261 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003262 upload.verbosity = 0
3263
3264 if fine_grained:
3265 # Process one branch synchronously to work through authentication, then
3266 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003267 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003268 def fetch(cl):
3269 try:
3270 return (cl, cl.GetStatus())
3271 except:
3272 # See http://crbug.com/629863.
3273 logging.exception('failed to fetch status for %s:', cl)
3274 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003275 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003276
tandriiea9514a2016-08-17 12:32:37 -07003277 changes_to_fetch = changes[1:]
3278 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003279 # Exit early if there was only one branch to fetch.
3280 return
3281
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003282 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003283 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003284 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003285 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003286
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003287 fetched_cls = set()
3288 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003289 while True:
3290 try:
3291 row = it.next(timeout=5)
3292 except multiprocessing.TimeoutError:
3293 break
3294
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003295 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003296 yield row
3297
3298 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003299 for cl in set(changes_to_fetch) - fetched_cls:
3300 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003301
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003302 else:
3303 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003304 for cl in changes:
3305 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003306
rmistry@google.com2dd99862015-06-22 12:22:18 +00003307
3308def upload_branch_deps(cl, args):
3309 """Uploads CLs of local branches that are dependents of the current branch.
3310
3311 If the local branch dependency tree looks like:
3312 test1 -> test2.1 -> test3.1
3313 -> test3.2
3314 -> test2.2 -> test3.3
3315
3316 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3317 run on the dependent branches in this order:
3318 test2.1, test3.1, test3.2, test2.2, test3.3
3319
3320 Note: This function does not rebase your local dependent branches. Use it when
3321 you make a change to the parent branch that will not conflict with its
3322 dependent branches, and you would like their dependencies updated in
3323 Rietveld.
3324 """
3325 if git_common.is_dirty_git_tree('upload-branch-deps'):
3326 return 1
3327
3328 root_branch = cl.GetBranch()
3329 if root_branch is None:
3330 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3331 'Get on a branch!')
3332 if not cl.GetIssue() or not cl.GetPatchset():
3333 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3334 'patchset dependencies without an uploaded CL.')
3335
3336 branches = RunGit(['for-each-ref',
3337 '--format=%(refname:short) %(upstream:short)',
3338 'refs/heads'])
3339 if not branches:
3340 print('No local branches found.')
3341 return 0
3342
3343 # Create a dictionary of all local branches to the branches that are dependent
3344 # on it.
3345 tracked_to_dependents = collections.defaultdict(list)
3346 for b in branches.splitlines():
3347 tokens = b.split()
3348 if len(tokens) == 2:
3349 branch_name, tracked = tokens
3350 tracked_to_dependents[tracked].append(branch_name)
3351
vapiera7fbd5a2016-06-16 09:17:49 -07003352 print()
3353 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003354 dependents = []
3355 def traverse_dependents_preorder(branch, padding=''):
3356 dependents_to_process = tracked_to_dependents.get(branch, [])
3357 padding += ' '
3358 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003359 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003360 dependents.append(dependent)
3361 traverse_dependents_preorder(dependent, padding)
3362 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003363 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003364
3365 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003366 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003367 return 0
3368
vapiera7fbd5a2016-06-16 09:17:49 -07003369 print('This command will checkout all dependent branches and run '
3370 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003371 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3372
andybons@chromium.org962f9462016-02-03 20:00:42 +00003373 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003374 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003375 args.extend(['-t', 'Updated patchset dependency'])
3376
rmistry@google.com2dd99862015-06-22 12:22:18 +00003377 # Record all dependents that failed to upload.
3378 failures = {}
3379 # Go through all dependents, checkout the branch and upload.
3380 try:
3381 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003382 print()
3383 print('--------------------------------------')
3384 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003385 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003386 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003387 try:
3388 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003389 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003390 failures[dependent_branch] = 1
3391 except: # pylint: disable=W0702
3392 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003393 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003394 finally:
3395 # Swap back to the original root branch.
3396 RunGit(['checkout', '-q', root_branch])
3397
vapiera7fbd5a2016-06-16 09:17:49 -07003398 print()
3399 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003400 for dependent_branch in dependents:
3401 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003402 print(' %s : %s' % (dependent_branch, upload_status))
3403 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003404
3405 return 0
3406
3407
kmarshall3bff56b2016-06-06 18:31:47 -07003408def CMDarchive(parser, args):
3409 """Archives and deletes branches associated with closed changelists."""
3410 parser.add_option(
3411 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003412 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003413 parser.add_option(
3414 '-f', '--force', action='store_true',
3415 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003416 parser.add_option(
3417 '-d', '--dry-run', action='store_true',
3418 help='Skip the branch tagging and removal steps.')
3419 parser.add_option(
3420 '-t', '--notags', action='store_true',
3421 help='Do not tag archived branches. '
3422 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003423
3424 auth.add_auth_options(parser)
3425 options, args = parser.parse_args(args)
3426 if args:
3427 parser.error('Unsupported args: %s' % ' '.join(args))
3428 auth_config = auth.extract_auth_config_from_options(options)
3429
3430 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3431 if not branches:
3432 return 0
3433
vapiera7fbd5a2016-06-16 09:17:49 -07003434 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003435 changes = [Changelist(branchref=b, auth_config=auth_config)
3436 for b in branches.splitlines()]
3437 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3438 statuses = get_cl_statuses(changes,
3439 fine_grained=True,
3440 max_processes=options.maxjobs)
3441 proposal = [(cl.GetBranch(),
3442 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3443 for cl, status in statuses
3444 if status == 'closed']
3445 proposal.sort()
3446
3447 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003448 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003449 return 0
3450
3451 current_branch = GetCurrentBranch()
3452
vapiera7fbd5a2016-06-16 09:17:49 -07003453 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003454 if options.notags:
3455 for next_item in proposal:
3456 print(' ' + next_item[0])
3457 else:
3458 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3459 for next_item in proposal:
3460 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003461
kmarshall9249e012016-08-23 12:02:16 -07003462 # Quit now on precondition failure or if instructed by the user, either
3463 # via an interactive prompt or by command line flags.
3464 if options.dry_run:
3465 print('\nNo changes were made (dry run).\n')
3466 return 0
3467 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003468 print('You are currently on a branch \'%s\' which is associated with a '
3469 'closed codereview issue, so archive cannot proceed. Please '
3470 'checkout another branch and run this command again.' %
3471 current_branch)
3472 return 1
kmarshall9249e012016-08-23 12:02:16 -07003473 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003474 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3475 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003476 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003477 return 1
3478
3479 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003480 if not options.notags:
3481 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003482 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003483
vapiera7fbd5a2016-06-16 09:17:49 -07003484 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003485
3486 return 0
3487
3488
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003490 """Show status of changelists.
3491
3492 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003493 - Red not sent for review or broken
3494 - Blue waiting for review
3495 - Yellow waiting for you to reply to review
3496 - Green LGTM'ed
3497 - Magenta in the commit queue
3498 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003499
3500 Also see 'git cl comments'.
3501 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003502 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003503 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003504 parser.add_option('-f', '--fast', action='store_true',
3505 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003506 parser.add_option(
3507 '-j', '--maxjobs', action='store', type=int,
3508 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003509
3510 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003511 _add_codereview_issue_select_options(
3512 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003513 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003514 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003515 if args:
3516 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003517 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518
iannuccie53c9352016-08-17 14:40:40 -07003519 if options.issue is not None and not options.field:
3520 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003521
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003522 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003523 cl = Changelist(auth_config=auth_config, issue=options.issue,
3524 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003526 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003527 elif options.field == 'id':
3528 issueid = cl.GetIssue()
3529 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003530 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003531 elif options.field == 'patch':
3532 patchset = cl.GetPatchset()
3533 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003534 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003535 elif options.field == 'status':
3536 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 elif options.field == 'url':
3538 url = cl.GetIssueURL()
3539 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003541 return 0
3542
3543 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3544 if not branches:
3545 print('No local branch found.')
3546 return 0
3547
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003548 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003549 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003550 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003552 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003553 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003554 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003555
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003556 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003557 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3558 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3559 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003560 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003561 c, status = output.next()
3562 branch_statuses[c.GetBranch()] = status
3563 status = branch_statuses.pop(branch)
3564 url = cl.GetIssueURL()
3565 if url and (not status or status == 'error'):
3566 # The issue probably doesn't exist anymore.
3567 url += ' (broken)'
3568
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003569 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003570 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003571 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003572 color = ''
3573 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003574 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003575 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003576 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003577 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003578
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003579 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003580 print()
3581 print('Current branch:',)
3582 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003583 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003584 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003585 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003587 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003588 print('Issue description:')
3589 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590 return 0
3591
3592
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003593def colorize_CMDstatus_doc():
3594 """To be called once in main() to add colors to git cl status help."""
3595 colors = [i for i in dir(Fore) if i[0].isupper()]
3596
3597 def colorize_line(line):
3598 for color in colors:
3599 if color in line.upper():
3600 # Extract whitespaces first and the leading '-'.
3601 indent = len(line) - len(line.lstrip(' ')) + 1
3602 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3603 return line
3604
3605 lines = CMDstatus.__doc__.splitlines()
3606 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3607
3608
phajdan.jre328cf92016-08-22 04:12:17 -07003609def write_json(path, contents):
3610 with open(path, 'w') as f:
3611 json.dump(contents, f)
3612
3613
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003614@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003615def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003616 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617
3618 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003619 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003620 parser.add_option('-r', '--reverse', action='store_true',
3621 help='Lookup the branch(es) for the specified issues. If '
3622 'no issues are specified, all branches with mapped '
3623 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003624 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003625 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003626 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003627 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003628
dnj@chromium.org406c4402015-03-03 17:22:28 +00003629 if options.reverse:
3630 branches = RunGit(['for-each-ref', 'refs/heads',
3631 '--format=%(refname:short)']).splitlines()
3632
3633 # Reverse issue lookup.
3634 issue_branch_map = {}
3635 for branch in branches:
3636 cl = Changelist(branchref=branch)
3637 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3638 if not args:
3639 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003640 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003641 for issue in args:
3642 if not issue:
3643 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003644 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003645 print('Branch for issue number %s: %s' % (
3646 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003647 if options.json:
3648 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003649 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003650 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003651 if len(args) > 0:
3652 try:
3653 issue = int(args[0])
3654 except ValueError:
3655 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003656 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003657 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003658 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003659 if options.json:
3660 write_json(options.json, {
3661 'issue': cl.GetIssue(),
3662 'issue_url': cl.GetIssueURL(),
3663 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664 return 0
3665
3666
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003667def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003668 """Shows or posts review comments for any changelist."""
3669 parser.add_option('-a', '--add-comment', dest='comment',
3670 help='comment to add to an issue')
3671 parser.add_option('-i', dest='issue',
3672 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003673 parser.add_option('-j', '--json-file',
3674 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003675 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003676 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003677 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003678
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003679 issue = None
3680 if options.issue:
3681 try:
3682 issue = int(options.issue)
3683 except ValueError:
3684 DieWithError('A review issue id is expected to be a number')
3685
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003686 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003687
3688 if options.comment:
3689 cl.AddComment(options.comment)
3690 return 0
3691
3692 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003693 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003694 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003695 summary.append({
3696 'date': message['date'],
3697 'lgtm': False,
3698 'message': message['text'],
3699 'not_lgtm': False,
3700 'sender': message['sender'],
3701 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003702 if message['disapproval']:
3703 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003704 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003705 elif message['approval']:
3706 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003707 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003708 elif message['sender'] == data['owner_email']:
3709 color = Fore.MAGENTA
3710 else:
3711 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003713 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003714 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003715 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003716 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003717 if options.json_file:
3718 with open(options.json_file, 'wb') as f:
3719 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003720 return 0
3721
3722
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003723@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003724def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003725 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003726 parser.add_option('-d', '--display', action='store_true',
3727 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003728 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003729 help='New description to set for this issue (- for stdin, '
3730 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003731 parser.add_option('-f', '--force', action='store_true',
3732 help='Delete any unpublished Gerrit edits for this issue '
3733 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003734
3735 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003736 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003737 options, args = parser.parse_args(args)
3738 _process_codereview_select_options(parser, options)
3739
3740 target_issue = None
3741 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003742 target_issue = ParseIssueNumberArgument(args[0])
3743 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003744 parser.print_help()
3745 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003746
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003747 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003748
martiniss6eda05f2016-06-30 10:18:35 -07003749 kwargs = {
3750 'auth_config': auth_config,
3751 'codereview': options.forced_codereview,
3752 }
3753 if target_issue:
3754 kwargs['issue'] = target_issue.issue
3755 if options.forced_codereview == 'rietveld':
3756 kwargs['rietveld_server'] = target_issue.hostname
3757
3758 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003759
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003760 if not cl.GetIssue():
3761 DieWithError('This branch has no associated changelist.')
3762 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003763
smut@google.com34fb6b12015-07-13 20:03:26 +00003764 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003765 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003766 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003767
3768 if options.new_description:
3769 text = options.new_description
3770 if text == '-':
3771 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003772 elif text == '+':
3773 base_branch = cl.GetCommonAncestorWithUpstream()
3774 change = cl.GetChange(base_branch, None, local_description=True)
3775 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003776
3777 description.set_description(text)
3778 else:
3779 description.prompt()
3780
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003781 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003782 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003783 return 0
3784
3785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786def CreateDescriptionFromLog(args):
3787 """Pulls out the commit log to use as a base for the CL description."""
3788 log_args = []
3789 if len(args) == 1 and not args[0].endswith('.'):
3790 log_args = [args[0] + '..']
3791 elif len(args) == 1 and args[0].endswith('...'):
3792 log_args = [args[0][:-1]]
3793 elif len(args) == 2:
3794 log_args = [args[0] + '..' + args[1]]
3795 else:
3796 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003797 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003798
3799
thestig@chromium.org44202a22014-03-11 19:22:18 +00003800def CMDlint(parser, args):
3801 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003802 parser.add_option('--filter', action='append', metavar='-x,+y',
3803 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003804 auth.add_auth_options(parser)
3805 options, args = parser.parse_args(args)
3806 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003807
3808 # Access to a protected member _XX of a client class
3809 # pylint: disable=W0212
3810 try:
3811 import cpplint
3812 import cpplint_chromium
3813 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003814 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003815 return 1
3816
3817 # Change the current working directory before calling lint so that it
3818 # shows the correct base.
3819 previous_cwd = os.getcwd()
3820 os.chdir(settings.GetRoot())
3821 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003822 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003823 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3824 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003825 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003827 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003828
3829 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003830 command = args + files
3831 if options.filter:
3832 command = ['--filter=' + ','.join(options.filter)] + command
3833 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003834
3835 white_regex = re.compile(settings.GetLintRegex())
3836 black_regex = re.compile(settings.GetLintIgnoreRegex())
3837 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3838 for filename in filenames:
3839 if white_regex.match(filename):
3840 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003842 else:
3843 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3844 extra_check_functions)
3845 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003847 finally:
3848 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003849 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003850 if cpplint._cpplint_state.error_count != 0:
3851 return 1
3852 return 0
3853
3854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003856 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003857 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003859 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003860 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003861 auth.add_auth_options(parser)
3862 options, args = parser.parse_args(args)
3863 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864
sbc@chromium.org71437c02015-04-09 19:29:40 +00003865 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003866 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867 return 1
3868
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003869 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870 if args:
3871 base_branch = args[0]
3872 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003873 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003874 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003875
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003876 cl.RunHook(
3877 committing=not options.upload,
3878 may_prompt=False,
3879 verbose=options.verbose,
3880 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003881 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882
3883
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003884def GenerateGerritChangeId(message):
3885 """Returns Ixxxxxx...xxx change id.
3886
3887 Works the same way as
3888 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3889 but can be called on demand on all platforms.
3890
3891 The basic idea is to generate git hash of a state of the tree, original commit
3892 message, author/committer info and timestamps.
3893 """
3894 lines = []
3895 tree_hash = RunGitSilent(['write-tree'])
3896 lines.append('tree %s' % tree_hash.strip())
3897 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3898 if code == 0:
3899 lines.append('parent %s' % parent.strip())
3900 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3901 lines.append('author %s' % author.strip())
3902 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3903 lines.append('committer %s' % committer.strip())
3904 lines.append('')
3905 # Note: Gerrit's commit-hook actually cleans message of some lines and
3906 # whitespace. This code is not doing this, but it clearly won't decrease
3907 # entropy.
3908 lines.append(message)
3909 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3910 stdin='\n'.join(lines))
3911 return 'I%s' % change_hash.strip()
3912
3913
wittman@chromium.org455dc922015-01-26 20:15:50 +00003914def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3915 """Computes the remote branch ref to use for the CL.
3916
3917 Args:
3918 remote (str): The git remote for the CL.
3919 remote_branch (str): The git remote branch for the CL.
3920 target_branch (str): The target branch specified by the user.
3921 pending_prefix (str): The pending prefix from the settings.
3922 """
3923 if not (remote and remote_branch):
3924 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003925
wittman@chromium.org455dc922015-01-26 20:15:50 +00003926 if target_branch:
3927 # Cannonicalize branch references to the equivalent local full symbolic
3928 # refs, which are then translated into the remote full symbolic refs
3929 # below.
3930 if '/' not in target_branch:
3931 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3932 else:
3933 prefix_replacements = (
3934 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3935 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3936 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3937 )
3938 match = None
3939 for regex, replacement in prefix_replacements:
3940 match = re.search(regex, target_branch)
3941 if match:
3942 remote_branch = target_branch.replace(match.group(0), replacement)
3943 break
3944 if not match:
3945 # This is a branch path but not one we recognize; use as-is.
3946 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003947 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3948 # Handle the refs that need to land in different refs.
3949 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003950
wittman@chromium.org455dc922015-01-26 20:15:50 +00003951 # Create the true path to the remote branch.
3952 # Does the following translation:
3953 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3954 # * refs/remotes/origin/master -> refs/heads/master
3955 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3956 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3957 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3958 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3959 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3960 'refs/heads/')
3961 elif remote_branch.startswith('refs/remotes/branch-heads'):
3962 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3963 # If a pending prefix exists then replace refs/ with it.
3964 if pending_prefix:
3965 remote_branch = remote_branch.replace('refs/', pending_prefix)
3966 return remote_branch
3967
3968
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003969def cleanup_list(l):
3970 """Fixes a list so that comma separated items are put as individual items.
3971
3972 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3973 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3974 """
3975 items = sum((i.split(',') for i in l), [])
3976 stripped_items = (i.strip() for i in items)
3977 return sorted(filter(None, stripped_items))
3978
3979
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003980@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003981def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003982 """Uploads the current changelist to codereview.
3983
3984 Can skip dependency patchset uploads for a branch by running:
3985 git config branch.branch_name.skip-deps-uploads True
3986 To unset run:
3987 git config --unset branch.branch_name.skip-deps-uploads
3988 Can also set the above globally by using the --global flag.
3989 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003990 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3991 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003992 parser.add_option('--bypass-watchlists', action='store_true',
3993 dest='bypass_watchlists',
3994 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003995 parser.add_option('-f', action='store_true', dest='force',
3996 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003997 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003998 parser.add_option('-b', '--bug',
3999 help='pre-populate the bug number(s) for this issue. '
4000 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004001 parser.add_option('--message-file', dest='message_file',
4002 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004003 parser.add_option('-t', dest='title',
4004 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004005 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004006 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004007 help='reviewer email addresses')
4008 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004009 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004010 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004011 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004012 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004013 parser.add_option('--emulate_svn_auto_props',
4014 '--emulate-svn-auto-props',
4015 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004016 dest="emulate_svn_auto_props",
4017 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004018 parser.add_option('-c', '--use-commit-queue', action='store_true',
4019 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004020 parser.add_option('--private', action='store_true',
4021 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004022 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004023 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004024 metavar='TARGET',
4025 help='Apply CL to remote ref TARGET. ' +
4026 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004027 parser.add_option('--squash', action='store_true',
4028 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004029 parser.add_option('--no-squash', action='store_true',
4030 help='Don\'t squash multiple commits into one ' +
4031 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004032 parser.add_option('--topic', default=None,
4033 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004034 parser.add_option('--email', default=None,
4035 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004036 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4037 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004038 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4039 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004040 help='Send the patchset to do a CQ dry run right after '
4041 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004042 parser.add_option('--dependencies', action='store_true',
4043 help='Uploads CLs of all the local branches that depend on '
4044 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004045
rmistry@google.com2dd99862015-06-22 12:22:18 +00004046 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004047 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004048 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004049 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004050 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004051 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004052 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004053
sbc@chromium.org71437c02015-04-09 19:29:40 +00004054 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004055 return 1
4056
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004057 options.reviewers = cleanup_list(options.reviewers)
4058 options.cc = cleanup_list(options.cc)
4059
tandriib80458a2016-06-23 12:20:07 -07004060 if options.message_file:
4061 if options.message:
4062 parser.error('only one of --message and --message-file allowed.')
4063 options.message = gclient_utils.FileRead(options.message_file)
4064 options.message_file = None
4065
tandrii4d0545a2016-07-06 03:56:49 -07004066 if options.cq_dry_run and options.use_commit_queue:
4067 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4068
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004069 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4070 settings.GetIsGerrit()
4071
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004072 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004073 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004074
4075
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004076def IsSubmoduleMergeCommit(ref):
4077 # When submodules are added to the repo, we expect there to be a single
4078 # non-git-svn merge commit at remote HEAD with a signature comment.
4079 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004080 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004081 return RunGit(cmd) != ''
4082
4083
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004085 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004087 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4088 upstream and closes the issue automatically and atomically.
4089
4090 Otherwise (in case of Rietveld):
4091 Squashes branch into a single commit.
4092 Updates changelog with metadata (e.g. pointer to review).
4093 Pushes/dcommits the code upstream.
4094 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095 """
4096 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4097 help='bypass upload presubmit hook')
4098 parser.add_option('-m', dest='message',
4099 help="override review description")
4100 parser.add_option('-f', action='store_true', dest='force',
4101 help="force yes to questions (don't prompt)")
4102 parser.add_option('-c', dest='contributor',
4103 help="external contributor for patch (appended to " +
4104 "description and used as author for git). Should be " +
4105 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004106 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004107 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004109 auth_config = auth.extract_auth_config_from_options(options)
4110
4111 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004113 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4114 if cl.IsGerrit():
4115 if options.message:
4116 # This could be implemented, but it requires sending a new patch to
4117 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4118 # Besides, Gerrit has the ability to change the commit message on submit
4119 # automatically, thus there is no need to support this option (so far?).
4120 parser.error('-m MESSAGE option is not supported for Gerrit.')
4121 if options.contributor:
4122 parser.error(
4123 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4124 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4125 'the contributor\'s "name <email>". If you can\'t upload such a '
4126 'commit for review, contact your repository admin and request'
4127 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004128 if not cl.GetIssue():
4129 DieWithError('You must upload the issue first to Gerrit.\n'
4130 ' If you would rather have `git cl land` upload '
4131 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004132 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4133 options.verbose)
4134
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004135 current = cl.GetBranch()
4136 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4137 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004138 print()
4139 print('Attempting to push branch %r into another local branch!' % current)
4140 print()
4141 print('Either reparent this branch on top of origin/master:')
4142 print(' git reparent-branch --root')
4143 print()
4144 print('OR run `git rebase-update` if you think the parent branch is ')
4145 print('already committed.')
4146 print()
4147 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004148 return 1
4149
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004150 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004151 # Default to merging against our best guess of the upstream branch.
4152 args = [cl.GetUpstreamBranch()]
4153
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004154 if options.contributor:
4155 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004157 return 1
4158
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004159 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004160 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004161
sbc@chromium.org71437c02015-04-09 19:29:40 +00004162 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004163 return 1
4164
4165 # This rev-list syntax means "show all commits not in my branch that
4166 # are in base_branch".
4167 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4168 base_branch]).splitlines()
4169 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print('Base branch "%s" has %d commits '
4171 'not in this branch.' % (base_branch, len(upstream_commits)))
4172 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173 return 1
4174
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004175 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004176 svn_head = None
4177 if cmd == 'dcommit' or base_has_submodules:
4178 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4179 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004182 # If the base_head is a submodule merge commit, the first parent of the
4183 # base_head should be a git-svn commit, which is what we're interested in.
4184 base_svn_head = base_branch
4185 if base_has_submodules:
4186 base_svn_head += '^1'
4187
4188 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004189 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004190 print('This branch has %d additional commits not upstreamed yet.'
4191 % len(extra_commits.splitlines()))
4192 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4193 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194 return 1
4195
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004196 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004197 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004198 author = None
4199 if options.contributor:
4200 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004201 hook_results = cl.RunHook(
4202 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004203 may_prompt=not options.force,
4204 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004205 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004206 if not hook_results.should_continue():
4207 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004209 # Check the tree status if the tree status URL is set.
4210 status = GetTreeStatus()
4211 if 'closed' == status:
4212 print('The tree is closed. Please wait for it to reopen. Use '
4213 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4214 return 1
4215 elif 'unknown' == status:
4216 print('Unable to determine tree status. Please verify manually and '
4217 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4218 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004220 change_desc = ChangeDescription(options.message)
4221 if not change_desc.description and cl.GetIssue():
4222 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004224 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004225 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004226 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004227 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004228 print('No description set.')
4229 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004230 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004232 # Keep a separate copy for the commit message, because the commit message
4233 # contains the link to the Rietveld issue, while the Rietveld message contains
4234 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004235 # Keep a separate copy for the commit message.
4236 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004237 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004238
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004239 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004240 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004241 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004242 # after it. Add a period on a new line to circumvent this. Also add a space
4243 # before the period to make sure that Gitiles continues to correctly resolve
4244 # the URL.
4245 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004247 commit_desc.append_footer('Patch from %s.' % options.contributor)
4248
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004249 print('Description:')
4250 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004252 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004253 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004254 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004256 # We want to squash all this branch's commits into one commit with the proper
4257 # description. We do this by doing a "reset --soft" to the base branch (which
4258 # keeps the working copy the same), then dcommitting that. If origin/master
4259 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4260 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004261 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004262 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4263 # Delete the branches if they exist.
4264 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4265 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4266 result = RunGitWithCode(showref_cmd)
4267 if result[0] == 0:
4268 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269
4270 # We might be in a directory that's present in this branch but not in the
4271 # trunk. Move up to the top of the tree so that git commands that expect a
4272 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004273 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004274 if rel_base_path:
4275 os.chdir(rel_base_path)
4276
4277 # Stuff our change into the merge branch.
4278 # We wrap in a try...finally block so if anything goes wrong,
4279 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004280 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004281 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004282 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004283 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004284 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004285 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004286 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004288 RunGit(
4289 [
4290 'commit', '--author', options.contributor,
4291 '-m', commit_desc.description,
4292 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004294 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004295 if base_has_submodules:
4296 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4297 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4298 RunGit(['checkout', CHERRY_PICK_BRANCH])
4299 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004300 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004301 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004302 mirror = settings.GetGitMirror(remote)
4303 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004304 pending_prefix = settings.GetPendingRefPrefix()
4305 if not pending_prefix or branch.startswith(pending_prefix):
4306 # If not using refs/pending/heads/* at all, or target ref is already set
4307 # to pending, then push to the target ref directly.
4308 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004309 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004310 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004311 else:
4312 # Cherry-pick the change on top of pending ref and then push it.
4313 assert branch.startswith('refs/'), branch
4314 assert pending_prefix[-1] == '/', pending_prefix
4315 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004316 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004317 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004318 if retcode == 0:
4319 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 else:
4321 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004322 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004323 'svn', 'dcommit',
4324 '-C%s' % options.similarity,
4325 '--no-rebase', '--rmdir',
4326 ]
4327 if settings.GetForceHttpsCommitUrl():
4328 # Allow forcing https commit URLs for some projects that don't allow
4329 # committing to http URLs (like Google Code).
4330 remote_url = cl.GetGitSvnRemoteUrl()
4331 if urlparse.urlparse(remote_url).scheme == 'http':
4332 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004333 cmd_args.append('--commit-url=%s' % remote_url)
4334 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004335 if 'Committed r' in output:
4336 revision = re.match(
4337 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4338 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339 finally:
4340 # And then swap back to the original branch and clean up.
4341 RunGit(['checkout', '-q', cl.GetBranch()])
4342 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004343 if base_has_submodules:
4344 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004346 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004347 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004348 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004349
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004350 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004351 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004352 try:
4353 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4354 # We set pushed_to_pending to False, since it made it all the way to the
4355 # real ref.
4356 pushed_to_pending = False
4357 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004358 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004359
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004361 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004363 if not to_pending:
4364 if viewvc_url and revision:
4365 change_desc.append_footer(
4366 'Committed: %s%s' % (viewvc_url, revision))
4367 elif revision:
4368 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print('Closing issue '
4370 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004371 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004372 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004373 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004374 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004375 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004376 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004377 if options.bypass_hooks:
4378 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4379 else:
4380 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004381 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004382
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004383 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004384 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print('The commit is in the pending queue (%s).' % pending_ref)
4386 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4387 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004388
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004389 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4390 if os.path.isfile(hook):
4391 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004392
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004393 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394
4395
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004396def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004397 print()
4398 print('Waiting for commit to be landed on %s...' % real_ref)
4399 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004400 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4401 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004402 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004403
4404 loop = 0
4405 while True:
4406 sys.stdout.write('fetching (%d)... \r' % loop)
4407 sys.stdout.flush()
4408 loop += 1
4409
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004410 if mirror:
4411 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004412 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4413 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4414 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4415 for commit in commits.splitlines():
4416 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004417 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004418 return commit
4419
4420 current_rev = to_rev
4421
4422
tandriibf429402016-09-14 07:09:12 -07004423def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004424 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4425
4426 Returns:
4427 (retcode of last operation, output log of last operation).
4428 """
4429 assert pending_ref.startswith('refs/'), pending_ref
4430 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4431 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4432 code = 0
4433 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004434 max_attempts = 3
4435 attempts_left = max_attempts
4436 while attempts_left:
4437 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004438 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004439 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004440
4441 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004442 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004443 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004444 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004445 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004446 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004447 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004449 continue
4450
4451 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004452 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004453 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004454 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004455 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004456 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4457 'the following files have merge conflicts:' % pending_ref)
4458 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4459 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004460 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004461 return code, out
4462
4463 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004464 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004465 code, out = RunGitWithCode(
4466 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4467 if code == 0:
4468 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004470 return code, out
4471
vapiera7fbd5a2016-06-16 09:17:49 -07004472 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004473 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004474 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004475 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004476 print('Fatal push error. Make sure your .netrc credentials and git '
4477 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004478 return code, out
4479
vapiera7fbd5a2016-06-16 09:17:49 -07004480 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004481 return code, out
4482
4483
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004484def IsFatalPushFailure(push_stdout):
4485 """True if retrying push won't help."""
4486 return '(prohibited by Gerrit)' in push_stdout
4487
4488
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004489@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004491 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004492 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004493 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004494 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004495 message = """This repository appears to be a git-svn mirror, but we
4496don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004497 else:
4498 message = """This doesn't appear to be an SVN repository.
4499If your project has a true, writeable git repository, you probably want to run
4500'git cl land' instead.
4501If your project has a git mirror of an upstream SVN master, you probably need
4502to run 'git svn init'.
4503
4504Using the wrong command might cause your commit to appear to succeed, and the
4505review to be closed, without actually landing upstream. If you choose to
4506proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004507 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004508 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004509 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4510 'Please let us know of this project you are committing to:'
4511 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004512 return SendUpstream(parser, args, 'dcommit')
4513
4514
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004515@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004516def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004517 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004518 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004519 print('This appears to be an SVN repository.')
4520 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004521 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004522 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004523 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004524
4525
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004526@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004527def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004528 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529 parser.add_option('-b', dest='newbranch',
4530 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004531 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004533 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4534 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004535 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004536 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004537 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004538 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004540 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004541
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004542
4543 group = optparse.OptionGroup(
4544 parser,
4545 'Options for continuing work on the current issue uploaded from a '
4546 'different clone (e.g. different machine). Must be used independently '
4547 'from the other options. No issue number should be specified, and the '
4548 'branch must have an issue number associated with it')
4549 group.add_option('--reapply', action='store_true', dest='reapply',
4550 help='Reset the branch and reapply the issue.\n'
4551 'CAUTION: This will undo any local changes in this '
4552 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004553
4554 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004555 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004556 parser.add_option_group(group)
4557
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004558 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004559 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004560 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004561 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004562 auth_config = auth.extract_auth_config_from_options(options)
4563
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004564
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004565 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004566 if options.newbranch:
4567 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004568 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004569 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004570
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004571 cl = Changelist(auth_config=auth_config,
4572 codereview=options.forced_codereview)
4573 if not cl.GetIssue():
4574 parser.error('current branch must have an associated issue')
4575
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004576 upstream = cl.GetUpstreamBranch()
4577 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004578 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004579
4580 RunGit(['reset', '--hard', upstream])
4581 if options.pull:
4582 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004583
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004584 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4585 options.directory)
4586
4587 if len(args) != 1 or not args[0]:
4588 parser.error('Must specify issue number or url')
4589
4590 # We don't want uncommitted changes mixed up with the patch.
4591 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004592 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004593
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004594 if options.newbranch:
4595 if options.force:
4596 RunGit(['branch', '-D', options.newbranch],
4597 stderr=subprocess2.PIPE, error_ok=True)
4598 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004599 elif not GetCurrentBranch():
4600 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004601
4602 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4603
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004604 if cl.IsGerrit():
4605 if options.reject:
4606 parser.error('--reject is not supported with Gerrit codereview.')
4607 if options.nocommit:
4608 parser.error('--nocommit is not supported with Gerrit codereview.')
4609 if options.directory:
4610 parser.error('--directory is not supported with Gerrit codereview.')
4611
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004612 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004613 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004614
4615
4616def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004617 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004618 # Provide a wrapper for git svn rebase to help avoid accidental
4619 # git svn dcommit.
4620 # It's the only command that doesn't use parser at all since we just defer
4621 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004622
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004623 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624
4625
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004626def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004627 """Fetches the tree status and returns either 'open', 'closed',
4628 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004629 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004630 if url:
4631 status = urllib2.urlopen(url).read().lower()
4632 if status.find('closed') != -1 or status == '0':
4633 return 'closed'
4634 elif status.find('open') != -1 or status == '1':
4635 return 'open'
4636 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637 return 'unset'
4638
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004640def GetTreeStatusReason():
4641 """Fetches the tree status from a json url and returns the message
4642 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004643 url = settings.GetTreeStatusUrl()
4644 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004645 connection = urllib2.urlopen(json_url)
4646 status = json.loads(connection.read())
4647 connection.close()
4648 return status['message']
4649
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004650
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004651def GetBuilderMaster(bot_list):
4652 """For a given builder, fetch the master from AE if available."""
4653 map_url = 'https://builders-map.appspot.com/'
4654 try:
4655 master_map = json.load(urllib2.urlopen(map_url))
4656 except urllib2.URLError as e:
4657 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4658 (map_url, e))
4659 except ValueError as e:
4660 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4661 if not master_map:
4662 return None, 'Failed to build master map.'
4663
4664 result_master = ''
4665 for bot in bot_list:
4666 builder = bot.split(':', 1)[0]
4667 master_list = master_map.get(builder, [])
4668 if not master_list:
4669 return None, ('No matching master for builder %s.' % builder)
4670 elif len(master_list) > 1:
4671 return None, ('The builder name %s exists in multiple masters %s.' %
4672 (builder, master_list))
4673 else:
4674 cur_master = master_list[0]
4675 if not result_master:
4676 result_master = cur_master
4677 elif result_master != cur_master:
4678 return None, 'The builders do not belong to the same master.'
4679 return result_master, None
4680
4681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004683 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004684 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004685 status = GetTreeStatus()
4686 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004687 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004688 return 2
4689
vapiera7fbd5a2016-06-16 09:17:49 -07004690 print('The tree is %s' % status)
4691 print()
4692 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693 if status != 'open':
4694 return 1
4695 return 0
4696
4697
maruel@chromium.org15192402012-09-06 12:38:29 +00004698def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004699 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4700 """
tandrii1838bad2016-10-06 00:10:52 -07004701 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004702 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004703 '-b', '--bot', action='append',
4704 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4705 'times to specify multiple builders. ex: '
4706 '"-b win_rel -b win_layout". See '
4707 'the try server waterfall for the builders name and the tests '
4708 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004709 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004710 '-B', '--bucket', default='',
4711 help=('Buildbucket bucket to send the try requests.'))
4712 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004713 '-m', '--master', default='',
4714 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004715 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004716 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004717 help='Revision to use for the try job; default: the revision will '
4718 'be determined by the try recipe that builder runs, which usually '
4719 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004720 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004721 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004722 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004723 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004724 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004725 '--project',
4726 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004727 'in recipe to determine to which repository or directory to '
4728 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004729 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004730 '-p', '--property', dest='properties', action='append', default=[],
4731 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004732 'key2=value2 etc. The value will be treated as '
4733 'json if decodable, or as string otherwise. '
4734 'NOTE: using this may make your try job not usable for CQ, '
4735 'which will then schedule another try job with default properties')
4736 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004737 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004738 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004739 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004740 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4741 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004742 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004743 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004744 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004745 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004746
machenbach@chromium.org45453142015-09-15 08:45:22 +00004747 # Make sure that all properties are prop=value pairs.
4748 bad_params = [x for x in options.properties if '=' not in x]
4749 if bad_params:
4750 parser.error('Got properties with missing "=": %s' % bad_params)
4751
maruel@chromium.org15192402012-09-06 12:38:29 +00004752 if args:
4753 parser.error('Unknown arguments: %s' % args)
4754
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004755 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004756 if not cl.GetIssue():
4757 parser.error('Need to upload first')
4758
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004759 if cl.IsGerrit():
4760 parser.error(
4761 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4762 'If your project has Commit Queue, dry run is a workaround:\n'
4763 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004764
tandriie113dfd2016-10-11 10:20:12 -07004765 error_message = cl.CannotTriggerTryJobReason()
4766 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004767 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004768
maruel@chromium.org15192402012-09-06 12:38:29 +00004769 if not options.name:
4770 options.name = cl.GetBranch()
4771
borenet6c0efe62016-10-19 08:13:29 -07004772 if options.bucket and options.master:
4773 parser.error('Only one of --bucket and --master may be used.')
4774
4775 if options.bot and not options.master and not options.bucket:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004776 options.master, err_msg = GetBuilderMaster(options.bot)
4777 if err_msg:
4778 parser.error('Tryserver master cannot be found because: %s\n'
4779 'Please manually specify the tryserver master'
4780 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004781
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004782 def GetMasterMap():
nodir70d11c32016-10-24 11:48:00 -07004783 """Returns {master: {builder_name: [test_names]}}. Not buckets!"""
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004784 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004785 if not options.bot:
4786 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004787
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004788 # Get try masters from PRESUBMIT.py files.
4789 masters = presubmit_support.DoGetTryMasters(
4790 change,
4791 change.LocalPaths(),
4792 settings.GetRoot(),
4793 None,
4794 None,
4795 options.verbose,
4796 sys.stdout)
4797 if masters:
4798 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004799
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004800 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4801 options.bot = presubmit_support.DoGetTrySlaves(
4802 change,
4803 change.LocalPaths(),
4804 settings.GetRoot(),
4805 None,
4806 None,
4807 options.verbose,
4808 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004809
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004810 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004811 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004812
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004813 builders_and_tests = {}
4814 # TODO(machenbach): The old style command-line options don't support
4815 # multiple try masters yet.
4816 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4817 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4818
4819 for bot in old_style:
4820 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004821 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004822 elif ',' in bot:
4823 parser.error('Specify one bot per --bot flag')
4824 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004825 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004826
4827 for bot, tests in new_style:
4828 builders_and_tests.setdefault(bot, []).extend(tests)
4829
4830 # Return a master map with one master to be backwards compatible. The
4831 # master name defaults to an empty string, which will cause the master
4832 # not to be set on rietveld (deprecated).
nodir70d11c32016-10-24 11:48:00 -07004833 return {options.master: builders_and_tests}
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004834
borenet6c0efe62016-10-19 08:13:29 -07004835 if options.bucket:
4836 buckets = {options.bucket: {b: [] for b in options.bot}}
4837 else:
nodir70d11c32016-10-24 11:48:00 -07004838 buckets = {}
4839 for master, data in GetMasterMap().iteritems():
4840 # Add the "master." prefix to the master name to obtain the bucket name.
4841 bucket = _prefix_master(master) if master else ''
4842 buckets[bucket] = data
4843
borenet6c0efe62016-10-19 08:13:29 -07004844 if not buckets:
4845 # Default to triggering Dry Run (see http://crbug.com/625697).
4846 if options.verbose:
4847 print('git cl try with no bots now defaults to CQ Dry Run.')
4848 try:
4849 cl.SetCQState(_CQState.DRY_RUN)
4850 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4851 return 0
4852 except KeyboardInterrupt:
4853 raise
4854 except:
4855 print('WARNING: failed to trigger CQ Dry Run.\n'
4856 'Either:\n'
4857 ' * your project has no CQ\n'
4858 ' * you don\'t have permission to trigger Dry Run\n'
4859 ' * bug in this code (see stack trace below).\n'
4860 'Consider specifying which bots to trigger manually '
4861 'or asking your project owners for permissions '
4862 'or contacting Chrome Infrastructure team at '
4863 'https://www.chromium.org/infra\n\n')
4864 # Still raise exception so that stack trace is printed.
4865 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004866
borenet6c0efe62016-10-19 08:13:29 -07004867 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004868 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004869 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004870 'of bot requires an initial job from a parent (usually a builder). '
4871 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004872 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004873 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004874
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004875 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004876 if patchset != cl.GetPatchset():
4877 print('Warning: Codereview server has newer patchsets (%s) than most '
4878 'recent upload from local checkout (%s). Did a previous upload '
4879 'fail?\n'
4880 'By default, git cl try uses the latest patchset from '
4881 'codereview, continuing to use patchset %s.\n' %
4882 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004883 try:
borenet6c0efe62016-10-19 08:13:29 -07004884 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4885 patchset)
tandrii568043b2016-10-11 07:49:18 -07004886 except BuildbucketResponseException as ex:
4887 print('ERROR: %s' % ex)
4888 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004889 return 0
4890
4891
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004892def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004893 """Prints info about try jobs associated with current CL."""
4894 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004895 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004896 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004897 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004898 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004899 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004900 '--color', action='store_true', default=setup_color.IS_TTY,
4901 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004902 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004903 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4904 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004905 group.add_option(
4906 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004907 parser.add_option_group(group)
4908 auth.add_auth_options(parser)
4909 options, args = parser.parse_args(args)
4910 if args:
4911 parser.error('Unrecognized args: %s' % ' '.join(args))
4912
4913 auth_config = auth.extract_auth_config_from_options(options)
4914 cl = Changelist(auth_config=auth_config)
4915 if not cl.GetIssue():
4916 parser.error('Need to upload first')
4917
tandrii221ab252016-10-06 08:12:04 -07004918 patchset = options.patchset
4919 if not patchset:
4920 patchset = cl.GetMostRecentPatchset()
4921 if not patchset:
4922 parser.error('Codereview doesn\'t know about issue %s. '
4923 'No access to issue or wrong issue number?\n'
4924 'Either upload first, or pass --patchset explicitely' %
4925 cl.GetIssue())
4926
4927 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004928 print('Warning: Codereview server has newer patchsets (%s) than most '
4929 'recent upload from local checkout (%s). Did a previous upload '
4930 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004931 'By default, git cl try-results uses the latest patchset from '
4932 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004933 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004934 try:
tandrii221ab252016-10-06 08:12:04 -07004935 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004936 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004937 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004938 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004939 if options.json:
4940 write_try_results_json(options.json, jobs)
4941 else:
4942 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004943 return 0
4944
4945
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004946@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004947def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004948 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004949 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004950 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004951 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004953 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004954 if args:
4955 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004956 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004957 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004958 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004959 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004960
4961 # Clear configured merge-base, if there is one.
4962 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004963 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004964 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004965 return 0
4966
4967
thestig@chromium.org00858c82013-12-02 23:08:03 +00004968def CMDweb(parser, args):
4969 """Opens the current CL in the web browser."""
4970 _, args = parser.parse_args(args)
4971 if args:
4972 parser.error('Unrecognized args: %s' % ' '.join(args))
4973
4974 issue_url = Changelist().GetIssueURL()
4975 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004976 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004977 return 1
4978
4979 webbrowser.open(issue_url)
4980 return 0
4981
4982
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004983def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004984 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004985 parser.add_option('-d', '--dry-run', action='store_true',
4986 help='trigger in dry run mode')
4987 parser.add_option('-c', '--clear', action='store_true',
4988 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004989 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004990 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004991 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004992 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004993 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004994 if args:
4995 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004996 if options.dry_run and options.clear:
4997 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4998
iannuccie53c9352016-08-17 14:40:40 -07004999 cl = Changelist(auth_config=auth_config, issue=options.issue,
5000 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005001 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005002 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005003 elif options.dry_run:
5004 state = _CQState.DRY_RUN
5005 else:
5006 state = _CQState.COMMIT
5007 if not cl.GetIssue():
5008 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005009 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005010 return 0
5011
5012
groby@chromium.org411034a2013-02-26 15:12:01 +00005013def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005014 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005015 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005016 auth.add_auth_options(parser)
5017 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005018 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005019 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005020 if args:
5021 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005022 cl = Changelist(auth_config=auth_config, issue=options.issue,
5023 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005024 # Ensure there actually is an issue to close.
5025 cl.GetDescription()
5026 cl.CloseIssue()
5027 return 0
5028
5029
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005030def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005031 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005032 parser.add_option(
5033 '--stat',
5034 action='store_true',
5035 dest='stat',
5036 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005037 auth.add_auth_options(parser)
5038 options, args = parser.parse_args(args)
5039 auth_config = auth.extract_auth_config_from_options(options)
5040 if args:
5041 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005042
5043 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005044 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005045 # Staged changes would be committed along with the patch from last
5046 # upload, hence counted toward the "last upload" side in the final
5047 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005048 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005049 return 1
5050
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005051 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005052 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005053 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005054 if not issue:
5055 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005056 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005057 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005058
5059 # Create a new branch based on the merge-base
5060 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005061 # Clear cached branch in cl object, to avoid overwriting original CL branch
5062 # properties.
5063 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005064 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005065 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005066 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005067 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005068 return rtn
5069
wychen@chromium.org06928532015-02-03 02:11:29 +00005070 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005071 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005072 cmd = ['git', 'diff']
5073 if options.stat:
5074 cmd.append('--stat')
5075 cmd.extend([TMP_BRANCH, branch, '--'])
5076 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005077 finally:
5078 RunGit(['checkout', '-q', branch])
5079 RunGit(['branch', '-D', TMP_BRANCH])
5080
5081 return 0
5082
5083
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005084def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005085 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005086 parser.add_option(
5087 '--no-color',
5088 action='store_true',
5089 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005090 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005091 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005092 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005093
5094 author = RunGit(['config', 'user.email']).strip() or None
5095
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005096 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005097
5098 if args:
5099 if len(args) > 1:
5100 parser.error('Unknown args')
5101 base_branch = args[0]
5102 else:
5103 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005104 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005105
5106 change = cl.GetChange(base_branch, None)
5107 return owners_finder.OwnersFinder(
5108 [f.LocalPath() for f in
5109 cl.GetChange(base_branch, None).AffectedFiles()],
5110 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005111 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005112 disable_color=options.no_color).run()
5113
5114
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005115def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005116 """Generates a diff command."""
5117 # Generate diff for the current branch's changes.
5118 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5119 upstream_commit, '--' ]
5120
5121 if args:
5122 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005123 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005124 diff_cmd.append(arg)
5125 else:
5126 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005127
5128 return diff_cmd
5129
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005130def MatchingFileType(file_name, extensions):
5131 """Returns true if the file name ends with one of the given extensions."""
5132 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005133
enne@chromium.org555cfe42014-01-29 18:21:39 +00005134@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005135def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005136 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005137 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005138 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005139 parser.add_option('--full', action='store_true',
5140 help='Reformat the full content of all touched files')
5141 parser.add_option('--dry-run', action='store_true',
5142 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005143 parser.add_option('--python', action='store_true',
5144 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005145 parser.add_option('--diff', action='store_true',
5146 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005147 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005148
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005149 # git diff generates paths against the root of the repository. Change
5150 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005151 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005152 if rel_base_path:
5153 os.chdir(rel_base_path)
5154
digit@chromium.org29e47272013-05-17 17:01:46 +00005155 # Grab the merge-base commit, i.e. the upstream commit of the current
5156 # branch when it was created or the last time it was rebased. This is
5157 # to cover the case where the user may have called "git fetch origin",
5158 # moving the origin branch to a newer commit, but hasn't rebased yet.
5159 upstream_commit = None
5160 cl = Changelist()
5161 upstream_branch = cl.GetUpstreamBranch()
5162 if upstream_branch:
5163 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5164 upstream_commit = upstream_commit.strip()
5165
5166 if not upstream_commit:
5167 DieWithError('Could not find base commit for this branch. '
5168 'Are you in detached state?')
5169
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005170 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5171 diff_output = RunGit(changed_files_cmd)
5172 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005173 # Filter out files deleted by this CL
5174 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005175
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005176 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5177 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5178 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005179 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005180
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005181 top_dir = os.path.normpath(
5182 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5183
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005184 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5185 # formatted. This is used to block during the presubmit.
5186 return_value = 0
5187
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005188 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005189 # Locate the clang-format binary in the checkout
5190 try:
5191 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005192 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005193 DieWithError(e)
5194
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005195 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005196 cmd = [clang_format_tool]
5197 if not opts.dry_run and not opts.diff:
5198 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005199 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005200 if opts.diff:
5201 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005202 else:
5203 env = os.environ.copy()
5204 env['PATH'] = str(os.path.dirname(clang_format_tool))
5205 try:
5206 script = clang_format.FindClangFormatScriptInChromiumTree(
5207 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005208 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005209 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005210
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005211 cmd = [sys.executable, script, '-p0']
5212 if not opts.dry_run and not opts.diff:
5213 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005214
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005215 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5216 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005217
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005218 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5219 if opts.diff:
5220 sys.stdout.write(stdout)
5221 if opts.dry_run and len(stdout) > 0:
5222 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005223
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005224 # Similar code to above, but using yapf on .py files rather than clang-format
5225 # on C/C++ files
5226 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005227 yapf_tool = gclient_utils.FindExecutable('yapf')
5228 if yapf_tool is None:
5229 DieWithError('yapf not found in PATH')
5230
5231 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005232 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005233 cmd = [yapf_tool]
5234 if not opts.dry_run and not opts.diff:
5235 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005236 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005237 if opts.diff:
5238 sys.stdout.write(stdout)
5239 else:
5240 # TODO(sbc): yapf --lines mode still has some issues.
5241 # https://github.com/google/yapf/issues/154
5242 DieWithError('--python currently only works with --full')
5243
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005244 # Dart's formatter does not have the nice property of only operating on
5245 # modified chunks, so hard code full.
5246 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005247 try:
5248 command = [dart_format.FindDartFmtToolInChromiumTree()]
5249 if not opts.dry_run and not opts.diff:
5250 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005251 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005252
ppi@chromium.org6593d932016-03-03 15:41:15 +00005253 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005254 if opts.dry_run and stdout:
5255 return_value = 2
5256 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005257 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5258 'found in this checkout. Files in other languages are still '
5259 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005260
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005261 # Format GN build files. Always run on full build files for canonical form.
5262 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005263 cmd = ['gn', 'format' ]
5264 if opts.dry_run or opts.diff:
5265 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005266 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005267 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5268 shell=sys.platform == 'win32',
5269 cwd=top_dir)
5270 if opts.dry_run and gn_ret == 2:
5271 return_value = 2 # Not formatted.
5272 elif opts.diff and gn_ret == 2:
5273 # TODO this should compute and print the actual diff.
5274 print("This change has GN build file diff for " + gn_diff_file)
5275 elif gn_ret != 0:
5276 # For non-dry run cases (and non-2 return values for dry-run), a
5277 # nonzero error code indicates a failure, probably because the file
5278 # doesn't parse.
5279 DieWithError("gn format failed on " + gn_diff_file +
5280 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005281
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005282 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005283
5284
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005285@subcommand.usage('<codereview url or issue id>')
5286def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005287 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005288 _, args = parser.parse_args(args)
5289
5290 if len(args) != 1:
5291 parser.print_help()
5292 return 1
5293
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005294 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005295 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005296 parser.print_help()
5297 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005298 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005299
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005300 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005301 output = RunGit(['config', '--local', '--get-regexp',
5302 r'branch\..*\.%s' % issueprefix],
5303 error_ok=True)
5304 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005305 if issue == target_issue:
5306 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005307
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005308 branches = []
5309 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005310 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005311 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005312 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005313 return 1
5314 if len(branches) == 1:
5315 RunGit(['checkout', branches[0]])
5316 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005317 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005318 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005319 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005320 which = raw_input('Choose by index: ')
5321 try:
5322 RunGit(['checkout', branches[int(which)]])
5323 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005324 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005325 return 1
5326
5327 return 0
5328
5329
maruel@chromium.org29404b52014-09-08 22:58:00 +00005330def CMDlol(parser, args):
5331 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005332 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005333 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5334 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5335 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005336 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005337 return 0
5338
5339
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005340class OptionParser(optparse.OptionParser):
5341 """Creates the option parse and add --verbose support."""
5342 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005343 optparse.OptionParser.__init__(
5344 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005345 self.add_option(
5346 '-v', '--verbose', action='count', default=0,
5347 help='Use 2 times for more debugging info')
5348
5349 def parse_args(self, args=None, values=None):
5350 options, args = optparse.OptionParser.parse_args(self, args, values)
5351 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5352 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5353 return options, args
5354
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005355
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005356def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005357 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005358 print('\nYour python version %s is unsupported, please upgrade.\n' %
5359 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005360 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005361
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005362 # Reload settings.
5363 global settings
5364 settings = Settings()
5365
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005366 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005367 dispatcher = subcommand.CommandDispatcher(__name__)
5368 try:
5369 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005370 except auth.AuthenticationError as e:
5371 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005372 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005373 if e.code != 500:
5374 raise
5375 DieWithError(
5376 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5377 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005378 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005379
5380
5381if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005382 # These affect sys.stdout so do it outside of main() to simplify mocks in
5383 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005384 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005385 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005386 try:
5387 sys.exit(main(sys.argv[1:]))
5388 except KeyboardInterrupt:
5389 sys.stderr.write('interrupted\n')
5390 sys.exit(1)