blob: 5b0f00aeecec04589a5db91d4352584c79de5f73 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000044import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000045import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000046import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000047import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000048import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000049import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000050import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000052import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000053import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000056import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
tandrii7400cf02016-06-21 08:48:07 -070062__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063
tandrii9d2c7a32016-06-22 03:42:45 -070064COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070065DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
87
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070089 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000090 sys.exit(1)
91
92
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000093def GetNoGitPagerEnv():
94 env = os.environ.copy()
95 # 'cat' is a magical git string that disables pagers on all platforms.
96 env['GIT_PAGER'] = 'cat'
97 return env
98
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000099
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000102 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000103 except subprocess2.CalledProcessError as e:
104 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 'Command "%s" failed.\n%s' % (
108 ' '.join(args), error_message or e.stdout or ''))
109 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
112def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000114 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115
116
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000118 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700119 if suppress_stderr:
120 stderr = subprocess2.VOID
121 else:
122 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 try:
tandrii5d48c322016-08-18 16:19:37 -0700124 (out, _), code = subprocess2.communicate(['git'] + args,
125 env=GetNoGitPagerEnv(),
126 stdout=subprocess2.PIPE,
127 stderr=stderr)
128 return code, out
129 except subprocess2.CalledProcessError as e:
130 logging.debug('Failed running %s', args)
131 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000135 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000136 return RunGitWithCode(args, suppress_stderr=True)[1]
137
138
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000142 return (version.startswith(prefix) and
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144
145
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000146def BranchExists(branch):
147 """Return True if specified branch exists."""
148 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
149 suppress_stderr=True)
150 return not code
151
152
tandrii2a16b952016-10-19 07:09:44 -0700153def time_sleep(seconds):
154 # Use this so that it can be mocked in tests without interfering with python
155 # system machinery.
156 import time # Local import to discourage others from importing time globally.
157 return time.sleep(seconds)
158
159
maruel@chromium.org90541732011-04-01 17:54:18 +0000160def ask_for_data(prompt):
161 try:
162 return raw_input(prompt)
163 except KeyboardInterrupt:
164 # Hide the exception.
165 sys.exit(1)
166
167
tandrii5d48c322016-08-18 16:19:37 -0700168def _git_branch_config_key(branch, key):
169 """Helper method to return Git config key for a branch."""
170 assert branch, 'branch name is required to set git config for it'
171 return 'branch.%s.%s' % (branch, key)
172
173
174def _git_get_branch_config_value(key, default=None, value_type=str,
175 branch=False):
176 """Returns git config value of given or current branch if any.
177
178 Returns default in all other cases.
179 """
180 assert value_type in (int, str, bool)
181 if branch is False: # Distinguishing default arg value from None.
182 branch = GetCurrentBranch()
183
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000184 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700185 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000186
tandrii5d48c322016-08-18 16:19:37 -0700187 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700188 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700189 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700190 # git config also has --int, but apparently git config suffers from integer
191 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700192 args.append(_git_branch_config_key(branch, key))
193 code, out = RunGitWithCode(args)
194 if code == 0:
195 value = out.strip()
196 if value_type == int:
197 return int(value)
198 if value_type == bool:
199 return bool(value.lower() == 'true')
200 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 return default
202
203
tandrii5d48c322016-08-18 16:19:37 -0700204def _git_set_branch_config_value(key, value, branch=None, **kwargs):
205 """Sets the value or unsets if it's None of a git branch config.
206
207 Valid, though not necessarily existing, branch must be provided,
208 otherwise currently checked out branch is used.
209 """
210 if not branch:
211 branch = GetCurrentBranch()
212 assert branch, 'a branch name OR currently checked out branch is required'
213 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700214 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700215 if value is None:
216 args.append('--unset')
217 elif isinstance(value, bool):
218 args.append('--bool')
219 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700220 else:
tandrii33a46ff2016-08-23 05:53:40 -0700221 # git config also has --int, but apparently git config suffers from integer
222 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700223 value = str(value)
224 args.append(_git_branch_config_key(branch, key))
225 if value is not None:
226 args.append(value)
227 RunGit(args, **kwargs)
228
229
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000230def add_git_similarity(parser):
231 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700232 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000233 help='Sets the percentage that a pair of files need to match in order to'
234 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000235 parser.add_option(
236 '--find-copies', action='store_true',
237 help='Allows git to look for copies.')
238 parser.add_option(
239 '--no-find-copies', action='store_false', dest='find_copies',
240 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000241
242 old_parser_args = parser.parse_args
243 def Parse(args):
244 options, args = old_parser_args(args)
245
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000246 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700247 options.similarity = _git_get_branch_config_value(
248 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000249 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 print('Note: Saving similarity of %d%% in git config.'
251 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700252 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000253
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254 options.similarity = max(0, min(options.similarity, 100))
255
256 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700257 options.find_copies = _git_get_branch_config_value(
258 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000259 else:
tandrii5d48c322016-08-18 16:19:37 -0700260 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000261
262 print('Using %d%% similarity for rename/copy detection. '
263 'Override with --similarity.' % options.similarity)
264
265 return options, args
266 parser.parse_args = Parse
267
268
machenbach@chromium.org45453142015-09-15 08:45:22 +0000269def _get_properties_from_options(options):
270 properties = dict(x.split('=', 1) for x in options.properties)
271 for key, val in properties.iteritems():
272 try:
273 properties[key] = json.loads(val)
274 except ValueError:
275 pass # If a value couldn't be evaluated, treat it as a string.
276 return properties
277
278
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000279def _prefix_master(master):
280 """Convert user-specified master name to full master name.
281
282 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
283 name, while the developers always use shortened master name
284 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
285 function does the conversion for buildbucket migration.
286 """
borenet6c0efe62016-10-19 08:13:29 -0700287 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000288 return master
borenet6c0efe62016-10-19 08:13:29 -0700289 return '%s%s' % (MASTER_PREFIX, master)
290
291
292def _unprefix_master(bucket):
293 """Convert bucket name to shortened master name.
294
295 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
296 name, while the developers always use shortened master name
297 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
298 function does the conversion for buildbucket migration.
299 """
300 if bucket.startswith(MASTER_PREFIX):
301 return bucket[len(MASTER_PREFIX):]
302 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000303
304
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000305def _buildbucket_retry(operation_name, http, *args, **kwargs):
306 """Retries requests to buildbucket service and returns parsed json content."""
307 try_count = 0
308 while True:
309 response, content = http.request(*args, **kwargs)
310 try:
311 content_json = json.loads(content)
312 except ValueError:
313 content_json = None
314
315 # Buildbucket could return an error even if status==200.
316 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000317 error = content_json.get('error')
318 if error.get('code') == 403:
319 raise BuildbucketResponseException(
320 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000321 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000322 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000323 raise BuildbucketResponseException(msg)
324
325 if response.status == 200:
326 if not content_json:
327 raise BuildbucketResponseException(
328 'Buildbucket returns invalid json content: %s.\n'
329 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
330 content)
331 return content_json
332 if response.status < 500 or try_count >= 2:
333 raise httplib2.HttpLib2Error(content)
334
335 # status >= 500 means transient failures.
336 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700337 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000338 try_count += 1
339 assert False, 'unreachable'
340
341
borenet6c0efe62016-10-19 08:13:29 -0700342def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700343 category='git_cl_try', patchset=None):
344 assert changelist.GetIssue(), 'CL must be uploaded first'
345 codereview_url = changelist.GetCodereviewServer()
346 assert codereview_url, 'CL must be uploaded first'
347 patchset = patchset or changelist.GetMostRecentPatchset()
348 assert patchset, 'CL must be uploaded first'
349
350 codereview_host = urlparse.urlparse(codereview_url).hostname
351 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000352 http = authenticator.authorize(httplib2.Http())
353 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700354
355 # TODO(tandrii): consider caching Gerrit CL details just like
356 # _RietveldChangelistImpl does, then caching values in these two variables
357 # won't be necessary.
358 owner_email = changelist.GetIssueOwner()
359 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000360
361 buildbucket_put_url = (
362 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000363 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700364 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
365 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
366 hostname=codereview_host,
367 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700369 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
371 batch_req_body = {'builds': []}
372 print_text = []
373 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700374 for bucket, builders_and_tests in sorted(buckets.iteritems()):
375 print_text.append('Bucket: %s' % bucket)
376 master = None
377 if bucket.startswith(MASTER_PREFIX):
378 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000379 for builder, tests in sorted(builders_and_tests.iteritems()):
380 print_text.append(' %s: %s' % (builder, tests))
381 parameters = {
382 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000383 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700384 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000385 'revision': options.revision,
386 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000387 'properties': {
388 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700389 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700390 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000391 'patch_storage': 'rietveld',
392 'patchset': patchset,
393 'reason': options.name,
tandriide281ae2016-10-12 06:02:30 -0700394 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000395 },
396 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000397 if 'presubmit' in builder.lower():
398 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000399 if tests:
400 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700401 if extra_properties:
402 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000403 if options.clobber:
404 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700405
406 tags = [
407 'builder:%s' % builder,
408 'buildset:%s' % buildset,
409 'user_agent:git_cl_try',
410 ]
411 if master:
412 parameters['properties']['master'] = master
413 tags.append('master:%s' % master)
414
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000415 batch_req_body['builds'].append(
416 {
417 'bucket': bucket,
418 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700420 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000421 }
422 )
423
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000424 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700425 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000426 http,
427 buildbucket_put_url,
428 'PUT',
429 body=json.dumps(batch_req_body),
430 headers={'Content-Type': 'application/json'}
431 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000432 print_text.append('To see results here, run: git cl try-results')
433 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700434 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000435
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000436
tandrii221ab252016-10-06 08:12:04 -0700437def fetch_try_jobs(auth_config, changelist, buildbucket_host,
438 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700439 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000440
qyearsley53f48a12016-09-01 10:45:13 -0700441 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000442 """
tandrii221ab252016-10-06 08:12:04 -0700443 assert buildbucket_host
444 assert changelist.GetIssue(), 'CL must be uploaded first'
445 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
446 patchset = patchset or changelist.GetMostRecentPatchset()
447 assert patchset, 'CL must be uploaded first'
448
449 codereview_url = changelist.GetCodereviewServer()
450 codereview_host = urlparse.urlparse(codereview_url).hostname
451 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000452 if authenticator.has_cached_credentials():
453 http = authenticator.authorize(httplib2.Http())
454 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700455 print('Warning: Some results might be missing because %s' %
456 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700457 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 http = httplib2.Http()
459
460 http.force_exception_to_status_code = True
461
tandrii221ab252016-10-06 08:12:04 -0700462 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
463 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
464 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000465 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700466 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000467 params = {'tag': 'buildset:%s' % buildset}
468
469 builds = {}
470 while True:
471 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700472 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000473 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700474 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475 for build in content.get('builds', []):
476 builds[build['id']] = build
477 if 'next_cursor' in content:
478 params['start_cursor'] = content['next_cursor']
479 else:
480 break
481 return builds
482
483
qyearsleyeab3c042016-08-24 09:18:28 -0700484def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000485 """Prints nicely result of fetch_try_jobs."""
486 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700487 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000488 return
489
490 # Make a copy, because we'll be modifying builds dictionary.
491 builds = builds.copy()
492 builder_names_cache = {}
493
494 def get_builder(b):
495 try:
496 return builder_names_cache[b['id']]
497 except KeyError:
498 try:
499 parameters = json.loads(b['parameters_json'])
500 name = parameters['builder_name']
501 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700502 print('WARNING: failed to get builder name for build %s: %s' % (
503 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 name = None
505 builder_names_cache[b['id']] = name
506 return name
507
508 def get_bucket(b):
509 bucket = b['bucket']
510 if bucket.startswith('master.'):
511 return bucket[len('master.'):]
512 return bucket
513
514 if options.print_master:
515 name_fmt = '%%-%ds %%-%ds' % (
516 max(len(str(get_bucket(b))) for b in builds.itervalues()),
517 max(len(str(get_builder(b))) for b in builds.itervalues()))
518 def get_name(b):
519 return name_fmt % (get_bucket(b), get_builder(b))
520 else:
521 name_fmt = '%%-%ds' % (
522 max(len(str(get_builder(b))) for b in builds.itervalues()))
523 def get_name(b):
524 return name_fmt % get_builder(b)
525
526 def sort_key(b):
527 return b['status'], b.get('result'), get_name(b), b.get('url')
528
529 def pop(title, f, color=None, **kwargs):
530 """Pop matching builds from `builds` dict and print them."""
531
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000532 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 colorize = str
534 else:
535 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
536
537 result = []
538 for b in builds.values():
539 if all(b.get(k) == v for k, v in kwargs.iteritems()):
540 builds.pop(b['id'])
541 result.append(b)
542 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700543 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700545 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546
547 total = len(builds)
548 pop(status='COMPLETED', result='SUCCESS',
549 title='Successes:', color=Fore.GREEN,
550 f=lambda b: (get_name(b), b.get('url')))
551 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
552 title='Infra Failures:', color=Fore.MAGENTA,
553 f=lambda b: (get_name(b), b.get('url')))
554 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
555 title='Failures:', color=Fore.RED,
556 f=lambda b: (get_name(b), b.get('url')))
557 pop(status='COMPLETED', result='CANCELED',
558 title='Canceled:', color=Fore.MAGENTA,
559 f=lambda b: (get_name(b),))
560 pop(status='COMPLETED', result='FAILURE',
561 failure_reason='INVALID_BUILD_DEFINITION',
562 title='Wrong master/builder name:', color=Fore.MAGENTA,
563 f=lambda b: (get_name(b),))
564 pop(status='COMPLETED', result='FAILURE',
565 title='Other failures:',
566 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
567 pop(status='COMPLETED',
568 title='Other finished:',
569 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
570 pop(status='STARTED',
571 title='Started:', color=Fore.YELLOW,
572 f=lambda b: (get_name(b), b.get('url')))
573 pop(status='SCHEDULED',
574 title='Scheduled:',
575 f=lambda b: (get_name(b), 'id=%s' % b['id']))
576 # The last section is just in case buildbucket API changes OR there is a bug.
577 pop(title='Other:',
578 f=lambda b: (get_name(b), 'id=%s' % b['id']))
579 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700580 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581
582
qyearsley53f48a12016-09-01 10:45:13 -0700583def write_try_results_json(output_file, builds):
584 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
585
586 The input |builds| dict is assumed to be generated by Buildbucket.
587 Buildbucket documentation: http://goo.gl/G0s101
588 """
589
590 def convert_build_dict(build):
591 return {
592 'buildbucket_id': build.get('id'),
593 'status': build.get('status'),
594 'result': build.get('result'),
595 'bucket': build.get('bucket'),
596 'builder_name': json.loads(
597 build.get('parameters_json', '{}')).get('builder_name'),
598 'failure_reason': build.get('failure_reason'),
599 'url': build.get('url'),
600 }
601
602 converted = []
603 for _, build in sorted(builds.items()):
604 converted.append(convert_build_dict(build))
605 write_json(output_file, converted)
606
607
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000608def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
609 """Return the corresponding git ref if |base_url| together with |glob_spec|
610 matches the full |url|.
611
612 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
613 """
614 fetch_suburl, as_ref = glob_spec.split(':')
615 if allow_wildcards:
616 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
617 if glob_match:
618 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
619 # "branches/{472,597,648}/src:refs/remotes/svn/*".
620 branch_re = re.escape(base_url)
621 if glob_match.group(1):
622 branch_re += '/' + re.escape(glob_match.group(1))
623 wildcard = glob_match.group(2)
624 if wildcard == '*':
625 branch_re += '([^/]*)'
626 else:
627 # Escape and replace surrounding braces with parentheses and commas
628 # with pipe symbols.
629 wildcard = re.escape(wildcard)
630 wildcard = re.sub('^\\\\{', '(', wildcard)
631 wildcard = re.sub('\\\\,', '|', wildcard)
632 wildcard = re.sub('\\\\}$', ')', wildcard)
633 branch_re += wildcard
634 if glob_match.group(3):
635 branch_re += re.escape(glob_match.group(3))
636 match = re.match(branch_re, url)
637 if match:
638 return re.sub('\*$', match.group(1), as_ref)
639
640 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
641 if fetch_suburl:
642 full_url = base_url + '/' + fetch_suburl
643 else:
644 full_url = base_url
645 if full_url == url:
646 return as_ref
647 return None
648
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000649
iannucci@chromium.org79540052012-10-19 23:15:26 +0000650def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000651 """Prints statistics about the change to the user."""
652 # --no-ext-diff is broken in some versions of Git, so try to work around
653 # this by overriding the environment (but there is still a problem if the
654 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000655 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000656 if 'GIT_EXTERNAL_DIFF' in env:
657 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000658
659 if find_copies:
660 similarity_options = ['--find-copies-harder', '-l100000',
661 '-C%s' % similarity]
662 else:
663 similarity_options = ['-M%s' % similarity]
664
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000665 try:
666 stdout = sys.stdout.fileno()
667 except AttributeError:
668 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000669 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000670 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000671 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000672 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000673
674
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000675class BuildbucketResponseException(Exception):
676 pass
677
678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679class Settings(object):
680 def __init__(self):
681 self.default_server = None
682 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000683 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684 self.is_git_svn = None
685 self.svn_branch = None
686 self.tree_status_url = None
687 self.viewvc_url = None
688 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000689 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000690 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000691 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000692 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000693 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000694 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000695 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700696 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697
698 def LazyUpdateIfNeeded(self):
699 """Updates the settings from a codereview.settings file, if available."""
700 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000701 # The only value that actually changes the behavior is
702 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000703 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000704 error_ok=True
705 ).strip().lower()
706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000708 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 LoadCodereviewSettingsFromFile(cr_settings_file)
710 self.updated = True
711
712 def GetDefaultServerUrl(self, error_ok=False):
713 if not self.default_server:
714 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000715 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000716 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 if error_ok:
718 return self.default_server
719 if not self.default_server:
720 error_message = ('Could not find settings file. You must configure '
721 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000722 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 return self.default_server
725
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000726 @staticmethod
727 def GetRelativeRoot():
728 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000731 if self.root is None:
732 self.root = os.path.abspath(self.GetRelativeRoot())
733 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000735 def GetGitMirror(self, remote='origin'):
736 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000737 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000738 if not os.path.isdir(local_url):
739 return None
740 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
741 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
742 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
743 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
744 if mirror.exists():
745 return mirror
746 return None
747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 def GetIsGitSvn(self):
749 """Return true if this repo looks like it's using git-svn."""
750 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000751 if self.GetPendingRefPrefix():
752 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
753 self.is_git_svn = False
754 else:
755 # If you have any "svn-remote.*" config keys, we think you're using svn.
756 self.is_git_svn = RunGitWithCode(
757 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 return self.is_git_svn
759
760 def GetSVNBranch(self):
761 if self.svn_branch is None:
762 if not self.GetIsGitSvn():
763 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
764
765 # Try to figure out which remote branch we're based on.
766 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000767 # 1) iterate through our branch history and find the svn URL.
768 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769
770 # regexp matching the git-svn line that contains the URL.
771 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
772
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000773 # We don't want to go through all of history, so read a line from the
774 # pipe at a time.
775 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000776 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000777 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
778 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000779 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000780 for line in proc.stdout:
781 match = git_svn_re.match(line)
782 if match:
783 url = match.group(1)
784 proc.stdout.close() # Cut pipe.
785 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000787 if url:
788 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
789 remotes = RunGit(['config', '--get-regexp',
790 r'^svn-remote\..*\.url']).splitlines()
791 for remote in remotes:
792 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000794 remote = match.group(1)
795 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000796 rewrite_root = RunGit(
797 ['config', 'svn-remote.%s.rewriteRoot' % remote],
798 error_ok=True).strip()
799 if rewrite_root:
800 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000801 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000802 ['config', 'svn-remote.%s.fetch' % remote],
803 error_ok=True).strip()
804 if fetch_spec:
805 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
806 if self.svn_branch:
807 break
808 branch_spec = RunGit(
809 ['config', 'svn-remote.%s.branches' % remote],
810 error_ok=True).strip()
811 if branch_spec:
812 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
813 if self.svn_branch:
814 break
815 tag_spec = RunGit(
816 ['config', 'svn-remote.%s.tags' % remote],
817 error_ok=True).strip()
818 if tag_spec:
819 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
820 if self.svn_branch:
821 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
823 if not self.svn_branch:
824 DieWithError('Can\'t guess svn branch -- try specifying it on the '
825 'command line')
826
827 return self.svn_branch
828
829 def GetTreeStatusUrl(self, error_ok=False):
830 if not self.tree_status_url:
831 error_message = ('You must configure your tree status URL by running '
832 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 self.tree_status_url = self._GetRietveldConfig(
834 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835 return self.tree_status_url
836
837 def GetViewVCUrl(self):
838 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000839 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 return self.viewvc_url
841
rmistry@google.com90752582014-01-14 21:04:50 +0000842 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000843 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000844
rmistry@google.com78948ed2015-07-08 23:09:57 +0000845 def GetIsSkipDependencyUpload(self, branch_name):
846 """Returns true if specified branch should skip dep uploads."""
847 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
848 error_ok=True)
849
rmistry@google.com5626a922015-02-26 14:03:30 +0000850 def GetRunPostUploadHook(self):
851 run_post_upload_hook = self._GetRietveldConfig(
852 'run-post-upload-hook', error_ok=True)
853 return run_post_upload_hook == "True"
854
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000855 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000856 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000857
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000858 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000859 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000860
ukai@chromium.orge8077812012-02-03 03:41:46 +0000861 def GetIsGerrit(self):
862 """Return true if this repo is assosiated with gerrit code review system."""
863 if self.is_gerrit is None:
864 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
865 return self.is_gerrit
866
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000867 def GetSquashGerritUploads(self):
868 """Return true if uploads to Gerrit should be squashed by default."""
869 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700870 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
871 if self.squash_gerrit_uploads is None:
872 # Default is squash now (http://crbug.com/611892#c23).
873 self.squash_gerrit_uploads = not (
874 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
875 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000876 return self.squash_gerrit_uploads
877
tandriia60502f2016-06-20 02:01:53 -0700878 def GetSquashGerritUploadsOverride(self):
879 """Return True or False if codereview.settings should be overridden.
880
881 Returns None if no override has been defined.
882 """
883 # See also http://crbug.com/611892#c23
884 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
885 error_ok=True).strip()
886 if result == 'true':
887 return True
888 if result == 'false':
889 return False
890 return None
891
tandrii@chromium.org28253532016-04-14 13:46:56 +0000892 def GetGerritSkipEnsureAuthenticated(self):
893 """Return True if EnsureAuthenticated should not be done for Gerrit
894 uploads."""
895 if self.gerrit_skip_ensure_authenticated is None:
896 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000897 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000898 error_ok=True).strip() == 'true')
899 return self.gerrit_skip_ensure_authenticated
900
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000901 def GetGitEditor(self):
902 """Return the editor specified in the git config, or None if none is."""
903 if self.git_editor is None:
904 self.git_editor = self._GetConfig('core.editor', error_ok=True)
905 return self.git_editor or None
906
thestig@chromium.org44202a22014-03-11 19:22:18 +0000907 def GetLintRegex(self):
908 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
909 DEFAULT_LINT_REGEX)
910
911 def GetLintIgnoreRegex(self):
912 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
913 DEFAULT_LINT_IGNORE_REGEX)
914
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000915 def GetProject(self):
916 if not self.project:
917 self.project = self._GetRietveldConfig('project', error_ok=True)
918 return self.project
919
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000920 def GetForceHttpsCommitUrl(self):
921 if not self.force_https_commit_url:
922 self.force_https_commit_url = self._GetRietveldConfig(
923 'force-https-commit-url', error_ok=True)
924 return self.force_https_commit_url
925
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000926 def GetPendingRefPrefix(self):
927 if not self.pending_ref_prefix:
928 self.pending_ref_prefix = self._GetRietveldConfig(
929 'pending-ref-prefix', error_ok=True)
930 return self.pending_ref_prefix
931
tandriif46c20f2016-09-14 06:17:05 -0700932 def GetHasGitNumberFooter(self):
933 # TODO(tandrii): this has to be removed after Rietveld is read-only.
934 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
935 if not self.git_number_footer:
936 self.git_number_footer = self._GetRietveldConfig(
937 'git-number-footer', error_ok=True)
938 return self.git_number_footer
939
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000940 def _GetRietveldConfig(self, param, **kwargs):
941 return self._GetConfig('rietveld.' + param, **kwargs)
942
rmistry@google.com78948ed2015-07-08 23:09:57 +0000943 def _GetBranchConfig(self, branch_name, param, **kwargs):
944 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 def _GetConfig(self, param, **kwargs):
947 self.LazyUpdateIfNeeded()
948 return RunGit(['config', param], **kwargs).strip()
949
950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951def ShortBranchName(branch):
952 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000953 return branch.replace('refs/heads/', '', 1)
954
955
956def GetCurrentBranchRef():
957 """Returns branch ref (e.g., refs/heads/master) or None."""
958 return RunGit(['symbolic-ref', 'HEAD'],
959 stderr=subprocess2.VOID, error_ok=True).strip() or None
960
961
962def GetCurrentBranch():
963 """Returns current branch or None.
964
965 For refs/heads/* branches, returns just last part. For others, full ref.
966 """
967 branchref = GetCurrentBranchRef()
968 if branchref:
969 return ShortBranchName(branchref)
970 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971
972
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000973class _CQState(object):
974 """Enum for states of CL with respect to Commit Queue."""
975 NONE = 'none'
976 DRY_RUN = 'dry_run'
977 COMMIT = 'commit'
978
979 ALL_STATES = [NONE, DRY_RUN, COMMIT]
980
981
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000982class _ParsedIssueNumberArgument(object):
983 def __init__(self, issue=None, patchset=None, hostname=None):
984 self.issue = issue
985 self.patchset = patchset
986 self.hostname = hostname
987
988 @property
989 def valid(self):
990 return self.issue is not None
991
992
993class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
994 def __init__(self, *args, **kwargs):
995 self.patch_url = kwargs.pop('patch_url', None)
996 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
997
998
999def ParseIssueNumberArgument(arg):
1000 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1001 fail_result = _ParsedIssueNumberArgument()
1002
1003 if arg.isdigit():
1004 return _ParsedIssueNumberArgument(issue=int(arg))
1005 if not arg.startswith('http'):
1006 return fail_result
1007 url = gclient_utils.UpgradeToHttps(arg)
1008 try:
1009 parsed_url = urlparse.urlparse(url)
1010 except ValueError:
1011 return fail_result
1012 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1013 tmp = cls.ParseIssueURL(parsed_url)
1014 if tmp is not None:
1015 return tmp
1016 return fail_result
1017
1018
tandriic2405f52016-10-10 08:13:15 -07001019class GerritIssueNotExists(Exception):
1020 def __init__(self, issue, url):
1021 self.issue = issue
1022 self.url = url
1023 super(GerritIssueNotExists, self).__init__()
1024
1025 def __str__(self):
1026 return 'issue %s at %s does not exist or you have no access to it' % (
1027 self.issue, self.url)
1028
1029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 """Changelist works with one changelist in local branch.
1032
1033 Supports two codereview backends: Rietveld or Gerrit, selected at object
1034 creation.
1035
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001036 Notes:
1037 * Not safe for concurrent multi-{thread,process} use.
1038 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001039 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001040 """
1041
1042 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1043 """Create a new ChangeList instance.
1044
1045 If issue is given, the codereview must be given too.
1046
1047 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1048 Otherwise, it's decided based on current configuration of the local branch,
1049 with default being 'rietveld' for backwards compatibility.
1050 See _load_codereview_impl for more details.
1051
1052 **kwargs will be passed directly to codereview implementation.
1053 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001055 global settings
1056 if not settings:
1057 # Happens when git_cl.py is used as a utility library.
1058 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001059
1060 if issue:
1061 assert codereview, 'codereview must be known, if issue is known'
1062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063 self.branchref = branchref
1064 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001065 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 self.branch = ShortBranchName(self.branchref)
1067 else:
1068 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001070 self.lookedup_issue = False
1071 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 self.has_description = False
1073 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001074 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001076 self.cc = None
1077 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001078 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001079
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001080 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001081 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001082 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001083 assert self._codereview_impl
1084 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001085
1086 def _load_codereview_impl(self, codereview=None, **kwargs):
1087 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001088 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1089 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1090 self._codereview = codereview
1091 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001092 return
1093
1094 # Automatic selection based on issue number set for a current branch.
1095 # Rietveld takes precedence over Gerrit.
1096 assert not self.issue
1097 # Whether we find issue or not, we are doing the lookup.
1098 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001099 if self.GetBranch():
1100 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1101 issue = _git_get_branch_config_value(
1102 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1103 if issue:
1104 self._codereview = codereview
1105 self._codereview_impl = cls(self, **kwargs)
1106 self.issue = int(issue)
1107 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001108
1109 # No issue is set for this branch, so decide based on repo-wide settings.
1110 return self._load_codereview_impl(
1111 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1112 **kwargs)
1113
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001114 def IsGerrit(self):
1115 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001116
1117 def GetCCList(self):
1118 """Return the users cc'd on this CL.
1119
agable92bec4f2016-08-24 09:27:27 -07001120 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001121 """
1122 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001123 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001124 more_cc = ','.join(self.watchers)
1125 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1126 return self.cc
1127
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001128 def GetCCListWithoutDefault(self):
1129 """Return the users cc'd on this CL excluding default ones."""
1130 if self.cc is None:
1131 self.cc = ','.join(self.watchers)
1132 return self.cc
1133
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001134 def SetWatchers(self, watchers):
1135 """Set the list of email addresses that should be cc'd based on the changed
1136 files in this CL.
1137 """
1138 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139
1140 def GetBranch(self):
1141 """Returns the short branch name, e.g. 'master'."""
1142 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001144 if not branchref:
1145 return None
1146 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.branch = ShortBranchName(self.branchref)
1148 return self.branch
1149
1150 def GetBranchRef(self):
1151 """Returns the full branch name, e.g. 'refs/heads/master'."""
1152 self.GetBranch() # Poke the lazy loader.
1153 return self.branchref
1154
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001155 def ClearBranch(self):
1156 """Clears cached branch data of this object."""
1157 self.branch = self.branchref = None
1158
tandrii5d48c322016-08-18 16:19:37 -07001159 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1160 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1161 kwargs['branch'] = self.GetBranch()
1162 return _git_get_branch_config_value(key, default, **kwargs)
1163
1164 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1165 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1166 assert self.GetBranch(), (
1167 'this CL must have an associated branch to %sset %s%s' %
1168 ('un' if value is None else '',
1169 key,
1170 '' if value is None else ' to %r' % value))
1171 kwargs['branch'] = self.GetBranch()
1172 return _git_set_branch_config_value(key, value, **kwargs)
1173
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001174 @staticmethod
1175 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001176 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 e.g. 'origin', 'refs/heads/master'
1178 """
1179 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001180 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001183 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001185 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1186 error_ok=True).strip()
1187 if upstream_branch:
1188 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001190 # Fall back on trying a git-svn upstream branch.
1191 if settings.GetIsGitSvn():
1192 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001194 # Else, try to guess the origin remote.
1195 remote_branches = RunGit(['branch', '-r']).split()
1196 if 'origin/master' in remote_branches:
1197 # Fall back on origin/master if it exits.
1198 remote = 'origin'
1199 upstream_branch = 'refs/heads/master'
1200 elif 'origin/trunk' in remote_branches:
1201 # Fall back on origin/trunk if it exists. Generally a shared
1202 # git-svn clone
1203 remote = 'origin'
1204 upstream_branch = 'refs/heads/trunk'
1205 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 DieWithError(
1207 'Unable to determine default branch to diff against.\n'
1208 'Either pass complete "git diff"-style arguments, like\n'
1209 ' git cl upload origin/master\n'
1210 'or verify this branch is set up to track another \n'
1211 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212
1213 return remote, upstream_branch
1214
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001215 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001216 upstream_branch = self.GetUpstreamBranch()
1217 if not BranchExists(upstream_branch):
1218 DieWithError('The upstream for the current branch (%s) does not exist '
1219 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001220 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001221 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001222
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 def GetUpstreamBranch(self):
1224 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001225 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001227 upstream_branch = upstream_branch.replace('refs/heads/',
1228 'refs/remotes/%s/' % remote)
1229 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1230 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 self.upstream_branch = upstream_branch
1232 return self.upstream_branch
1233
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001234 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001235 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001236 remote, branch = None, self.GetBranch()
1237 seen_branches = set()
1238 while branch not in seen_branches:
1239 seen_branches.add(branch)
1240 remote, branch = self.FetchUpstreamTuple(branch)
1241 branch = ShortBranchName(branch)
1242 if remote != '.' or branch.startswith('refs/remotes'):
1243 break
1244 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001245 remotes = RunGit(['remote'], error_ok=True).split()
1246 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001247 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001248 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001250 logging.warning('Could not determine which remote this change is '
1251 'associated with, so defaulting to "%s". This may '
1252 'not be what you want. You may prevent this message '
1253 'by running "git svn info" as documented here: %s',
1254 self._remote,
1255 GIT_INSTRUCTIONS_URL)
1256 else:
1257 logging.warn('Could not determine which remote this change is '
1258 'associated with. You may prevent this message by '
1259 'running "git svn info" as documented here: %s',
1260 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 branch = 'HEAD'
1262 if branch.startswith('refs/remotes'):
1263 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001264 elif branch.startswith('refs/branch-heads/'):
1265 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001266 else:
1267 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001268 return self._remote
1269
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 def GitSanityChecks(self, upstream_git_obj):
1271 """Checks git repo status and ensures diff is from local commits."""
1272
sbc@chromium.org79706062015-01-14 21:18:12 +00001273 if upstream_git_obj is None:
1274 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001275 print('ERROR: unable to determine current branch (detached HEAD?)',
1276 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001277 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001278 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001279 return False
1280
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 # Verify the commit we're diffing against is in our current branch.
1282 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1283 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1284 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001285 print('ERROR: %s is not in the current branch. You may need to rebase '
1286 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 return False
1288
1289 # List the commits inside the diff, and verify they are all local.
1290 commits_in_diff = RunGit(
1291 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1292 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1293 remote_branch = remote_branch.strip()
1294 if code != 0:
1295 _, remote_branch = self.GetRemoteBranch()
1296
1297 commits_in_remote = RunGit(
1298 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1299
1300 common_commits = set(commits_in_diff) & set(commits_in_remote)
1301 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001302 print('ERROR: Your diff contains %d commits already in %s.\n'
1303 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1304 'the diff. If you are using a custom git flow, you can override'
1305 ' the reference used for this check with "git config '
1306 'gitcl.remotebranch <git-ref>".' % (
1307 len(common_commits), remote_branch, upstream_git_obj),
1308 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 return False
1310 return True
1311
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001312 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001313 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001314
1315 Returns None if it is not set.
1316 """
tandrii5d48c322016-08-18 16:19:37 -07001317 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001318
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001319 def GetGitSvnRemoteUrl(self):
1320 """Return the configured git-svn remote URL parsed from git svn info.
1321
1322 Returns None if it is not set.
1323 """
1324 # URL is dependent on the current directory.
1325 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1326 if data:
1327 keys = dict(line.split(': ', 1) for line in data.splitlines()
1328 if ': ' in line)
1329 return keys.get('URL', None)
1330 return None
1331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 def GetRemoteUrl(self):
1333 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1334
1335 Returns None if there is no remote.
1336 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001338 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1339
1340 # If URL is pointing to a local directory, it is probably a git cache.
1341 if os.path.isdir(url):
1342 url = RunGit(['config', 'remote.%s.url' % remote],
1343 error_ok=True,
1344 cwd=url).strip()
1345 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001347 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001348 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001349 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001350 self.issue = self._GitGetBranchConfigValue(
1351 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001352 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 return self.issue
1354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 def GetIssueURL(self):
1356 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001357 issue = self.GetIssue()
1358 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001359 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
1362 def GetDescription(self, pretty=False):
1363 if not self.has_description:
1364 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001365 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 self.has_description = True
1367 if pretty:
1368 wrapper = textwrap.TextWrapper()
1369 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1370 return wrapper.fill(self.description)
1371 return self.description
1372
1373 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001374 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001375 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001376 self.patchset = self._GitGetBranchConfigValue(
1377 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return self.patchset
1380
1381 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001382 """Set this branch's patchset. If patchset=0, clears the patchset."""
1383 assert self.GetBranch()
1384 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001385 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001386 else:
1387 self.patchset = int(patchset)
1388 self._GitSetBranchConfigValue(
1389 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001391 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001392 """Set this branch's issue. If issue isn't given, clears the issue."""
1393 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001395 issue = int(issue)
1396 self._GitSetBranchConfigValue(
1397 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001399 codereview_server = self._codereview_impl.GetCodereviewServer()
1400 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001401 self._GitSetBranchConfigValue(
1402 self._codereview_impl.CodereviewServerConfigKey(),
1403 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 else:
tandrii5d48c322016-08-18 16:19:37 -07001405 # Reset all of these just to be clean.
1406 reset_suffixes = [
1407 'last-upload-hash',
1408 self._codereview_impl.IssueConfigKey(),
1409 self._codereview_impl.PatchsetConfigKey(),
1410 self._codereview_impl.CodereviewServerConfigKey(),
1411 ] + self._PostUnsetIssueProperties()
1412 for prop in reset_suffixes:
1413 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001415 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
dnjba1b0f32016-09-02 12:37:42 -07001417 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001418 if not self.GitSanityChecks(upstream_branch):
1419 DieWithError('\nGit sanity check failure')
1420
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001421 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001422 if not root:
1423 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001424 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001425
1426 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001427 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001428 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001429 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001430 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001431 except subprocess2.CalledProcessError:
1432 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001433 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001434 'This branch probably doesn\'t exist anymore. To reset the\n'
1435 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001436 ' git branch --set-upstream-to origin/master %s\n'
1437 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001438 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001439
maruel@chromium.org52424302012-08-29 15:14:30 +00001440 issue = self.GetIssue()
1441 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001442 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001443 description = self.GetDescription()
1444 else:
1445 # If the change was never uploaded, use the log messages of all commits
1446 # up to the branch point, as git cl upload will prefill the description
1447 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001448 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1449 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001450
1451 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001452 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001453 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001454 name,
1455 description,
1456 absroot,
1457 files,
1458 issue,
1459 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001460 author,
1461 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001462
dsansomee2d6fd92016-09-08 00:10:47 -07001463 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001465 return self._codereview_impl.UpdateDescriptionRemote(
1466 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467
1468 def RunHook(self, committing, may_prompt, verbose, change):
1469 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1470 try:
1471 return presubmit_support.DoPresubmitChecks(change, committing,
1472 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1473 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001474 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1475 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001476 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001477 DieWithError(
1478 ('%s\nMaybe your depot_tools is out of date?\n'
1479 'If all fails, contact maruel@') % e)
1480
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001481 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1482 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001483 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1484 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001485 else:
1486 # Assume url.
1487 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1488 urlparse.urlparse(issue_arg))
1489 if not parsed_issue_arg or not parsed_issue_arg.valid:
1490 DieWithError('Failed to parse issue argument "%s". '
1491 'Must be an issue number or a valid URL.' % issue_arg)
1492 return self._codereview_impl.CMDPatchWithParsedIssue(
1493 parsed_issue_arg, reject, nocommit, directory)
1494
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001495 def CMDUpload(self, options, git_diff_args, orig_args):
1496 """Uploads a change to codereview."""
1497 if git_diff_args:
1498 # TODO(ukai): is it ok for gerrit case?
1499 base_branch = git_diff_args[0]
1500 else:
1501 if self.GetBranch() is None:
1502 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1503
1504 # Default to diffing against common ancestor of upstream branch
1505 base_branch = self.GetCommonAncestorWithUpstream()
1506 git_diff_args = [base_branch, 'HEAD']
1507
1508 # Make sure authenticated to codereview before running potentially expensive
1509 # hooks. It is a fast, best efforts check. Codereview still can reject the
1510 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001511 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001512
1513 # Apply watchlists on upload.
1514 change = self.GetChange(base_branch, None)
1515 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1516 files = [f.LocalPath() for f in change.AffectedFiles()]
1517 if not options.bypass_watchlists:
1518 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1519
1520 if not options.bypass_hooks:
1521 if options.reviewers or options.tbr_owners:
1522 # Set the reviewer list now so that presubmit checks can access it.
1523 change_description = ChangeDescription(change.FullDescriptionText())
1524 change_description.update_reviewers(options.reviewers,
1525 options.tbr_owners,
1526 change)
1527 change.SetDescriptionText(change_description.description)
1528 hook_results = self.RunHook(committing=False,
1529 may_prompt=not options.force,
1530 verbose=options.verbose,
1531 change=change)
1532 if not hook_results.should_continue():
1533 return 1
1534 if not options.reviewers and hook_results.reviewers:
1535 options.reviewers = hook_results.reviewers.split(',')
1536
1537 if self.GetIssue():
1538 latest_patchset = self.GetMostRecentPatchset()
1539 local_patchset = self.GetPatchset()
1540 if (latest_patchset and local_patchset and
1541 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001542 print('The last upload made from this repository was patchset #%d but '
1543 'the most recent patchset on the server is #%d.'
1544 % (local_patchset, latest_patchset))
1545 print('Uploading will still work, but if you\'ve uploaded to this '
1546 'issue from another machine or branch the patch you\'re '
1547 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 ask_for_data('About to upload; enter to confirm.')
1549
1550 print_stats(options.similarity, options.find_copies, git_diff_args)
1551 ret = self.CMDUploadChange(options, git_diff_args, change)
1552 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001553 if options.use_commit_queue:
1554 self.SetCQState(_CQState.COMMIT)
1555 elif options.cq_dry_run:
1556 self.SetCQState(_CQState.DRY_RUN)
1557
tandrii5d48c322016-08-18 16:19:37 -07001558 _git_set_branch_config_value('last-upload-hash',
1559 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 # Run post upload hooks, if specified.
1561 if settings.GetRunPostUploadHook():
1562 presubmit_support.DoPostUploadExecuter(
1563 change,
1564 self,
1565 settings.GetRoot(),
1566 options.verbose,
1567 sys.stdout)
1568
1569 # Upload all dependencies if specified.
1570 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001571 print()
1572 print('--dependencies has been specified.')
1573 print('All dependent local branches will be re-uploaded.')
1574 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 # Remove the dependencies flag from args so that we do not end up in a
1576 # loop.
1577 orig_args.remove('--dependencies')
1578 ret = upload_branch_deps(self, orig_args)
1579 return ret
1580
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001581 def SetCQState(self, new_state):
1582 """Update the CQ state for latest patchset.
1583
1584 Issue must have been already uploaded and known.
1585 """
1586 assert new_state in _CQState.ALL_STATES
1587 assert self.GetIssue()
1588 return self._codereview_impl.SetCQState(new_state)
1589
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 # Forward methods to codereview specific implementation.
1591
1592 def CloseIssue(self):
1593 return self._codereview_impl.CloseIssue()
1594
1595 def GetStatus(self):
1596 return self._codereview_impl.GetStatus()
1597
1598 def GetCodereviewServer(self):
1599 return self._codereview_impl.GetCodereviewServer()
1600
tandriide281ae2016-10-12 06:02:30 -07001601 def GetIssueOwner(self):
1602 """Get owner from codereview, which may differ from this checkout."""
1603 return self._codereview_impl.GetIssueOwner()
1604
1605 def GetIssueProject(self):
1606 """Get project from codereview, which may differ from what this
1607 checkout's codereview.settings or gerrit project URL say.
1608 """
1609 return self._codereview_impl.GetIssueProject()
1610
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001611 def GetApprovingReviewers(self):
1612 return self._codereview_impl.GetApprovingReviewers()
1613
1614 def GetMostRecentPatchset(self):
1615 return self._codereview_impl.GetMostRecentPatchset()
1616
tandriide281ae2016-10-12 06:02:30 -07001617 def CannotTriggerTryJobReason(self):
1618 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1619 return self._codereview_impl.CannotTriggerTryJobReason()
1620
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001621 def __getattr__(self, attr):
1622 # This is because lots of untested code accesses Rietveld-specific stuff
1623 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001624 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001625 # Note that child method defines __getattr__ as well, and forwards it here,
1626 # because _RietveldChangelistImpl is not cleaned up yet, and given
1627 # deprecation of Rietveld, it should probably be just removed.
1628 # Until that time, avoid infinite recursion by bypassing __getattr__
1629 # of implementation class.
1630 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001631
1632
1633class _ChangelistCodereviewBase(object):
1634 """Abstract base class encapsulating codereview specifics of a changelist."""
1635 def __init__(self, changelist):
1636 self._changelist = changelist # instance of Changelist
1637
1638 def __getattr__(self, attr):
1639 # Forward methods to changelist.
1640 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1641 # _RietveldChangelistImpl to avoid this hack?
1642 return getattr(self._changelist, attr)
1643
1644 def GetStatus(self):
1645 """Apply a rough heuristic to give a simple summary of an issue's review
1646 or CQ status, assuming adherence to a common workflow.
1647
1648 Returns None if no issue for this branch, or specific string keywords.
1649 """
1650 raise NotImplementedError()
1651
1652 def GetCodereviewServer(self):
1653 """Returns server URL without end slash, like "https://codereview.com"."""
1654 raise NotImplementedError()
1655
1656 def FetchDescription(self):
1657 """Fetches and returns description from the codereview server."""
1658 raise NotImplementedError()
1659
tandrii5d48c322016-08-18 16:19:37 -07001660 @classmethod
1661 def IssueConfigKey(cls):
1662 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001663 raise NotImplementedError()
1664
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001665 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001666 def PatchsetConfigKey(cls):
1667 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668 raise NotImplementedError()
1669
tandrii5d48c322016-08-18 16:19:37 -07001670 @classmethod
1671 def CodereviewServerConfigKey(cls):
1672 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 raise NotImplementedError()
1674
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001675 def _PostUnsetIssueProperties(self):
1676 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001677 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001678
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001679 def GetRieveldObjForPresubmit(self):
1680 # This is an unfortunate Rietveld-embeddedness in presubmit.
1681 # For non-Rietveld codereviews, this probably should return a dummy object.
1682 raise NotImplementedError()
1683
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001684 def GetGerritObjForPresubmit(self):
1685 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1686 return None
1687
dsansomee2d6fd92016-09-08 00:10:47 -07001688 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 """Update the description on codereview site."""
1690 raise NotImplementedError()
1691
1692 def CloseIssue(self):
1693 """Closes the issue."""
1694 raise NotImplementedError()
1695
1696 def GetApprovingReviewers(self):
1697 """Returns a list of reviewers approving the change.
1698
1699 Note: not necessarily committers.
1700 """
1701 raise NotImplementedError()
1702
1703 def GetMostRecentPatchset(self):
1704 """Returns the most recent patchset number from the codereview site."""
1705 raise NotImplementedError()
1706
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001707 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1708 directory):
1709 """Fetches and applies the issue.
1710
1711 Arguments:
1712 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1713 reject: if True, reject the failed patch instead of switching to 3-way
1714 merge. Rietveld only.
1715 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1716 only.
1717 directory: switch to directory before applying the patch. Rietveld only.
1718 """
1719 raise NotImplementedError()
1720
1721 @staticmethod
1722 def ParseIssueURL(parsed_url):
1723 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1724 failed."""
1725 raise NotImplementedError()
1726
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001727 def EnsureAuthenticated(self, force):
1728 """Best effort check that user is authenticated with codereview server.
1729
1730 Arguments:
1731 force: whether to skip confirmation questions.
1732 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001733 raise NotImplementedError()
1734
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001735 def CMDUploadChange(self, options, args, change):
1736 """Uploads a change to codereview."""
1737 raise NotImplementedError()
1738
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001739 def SetCQState(self, new_state):
1740 """Update the CQ state for latest patchset.
1741
1742 Issue must have been already uploaded and known.
1743 """
1744 raise NotImplementedError()
1745
tandriie113dfd2016-10-11 10:20:12 -07001746 def CannotTriggerTryJobReason(self):
1747 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1748 raise NotImplementedError()
1749
tandriide281ae2016-10-12 06:02:30 -07001750 def GetIssueOwner(self):
1751 raise NotImplementedError()
1752
1753 def GetIssueProject(self):
1754 raise NotImplementedError()
1755
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756
1757class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1758 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1759 super(_RietveldChangelistImpl, self).__init__(changelist)
1760 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001761 if not rietveld_server:
1762 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763
1764 self._rietveld_server = rietveld_server
1765 self._auth_config = auth_config
1766 self._props = None
1767 self._rpc_server = None
1768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetCodereviewServer(self):
1770 if not self._rietveld_server:
1771 # If we're on a branch then get the server potentially associated
1772 # with that branch.
1773 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001774 self._rietveld_server = gclient_utils.UpgradeToHttps(
1775 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 if not self._rietveld_server:
1777 self._rietveld_server = settings.GetDefaultServerUrl()
1778 return self._rietveld_server
1779
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001780 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001781 """Best effort check that user is authenticated with Rietveld server."""
1782 if self._auth_config.use_oauth2:
1783 authenticator = auth.get_authenticator_for_host(
1784 self.GetCodereviewServer(), self._auth_config)
1785 if not authenticator.has_cached_credentials():
1786 raise auth.LoginRequiredError(self.GetCodereviewServer())
1787
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 def FetchDescription(self):
1789 issue = self.GetIssue()
1790 assert issue
1791 try:
1792 return self.RpcServer().get_description(issue).strip()
1793 except urllib2.HTTPError as e:
1794 if e.code == 404:
1795 DieWithError(
1796 ('\nWhile fetching the description for issue %d, received a '
1797 '404 (not found)\n'
1798 'error. It is likely that you deleted this '
1799 'issue on the server. If this is the\n'
1800 'case, please run\n\n'
1801 ' git cl issue 0\n\n'
1802 'to clear the association with the deleted issue. Then run '
1803 'this command again.') % issue)
1804 else:
1805 DieWithError(
1806 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1807 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001808 print('Warning: Failed to retrieve CL description due to network '
1809 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 return ''
1811
1812 def GetMostRecentPatchset(self):
1813 return self.GetIssueProperties()['patchsets'][-1]
1814
1815 def GetPatchSetDiff(self, issue, patchset):
1816 return self.RpcServer().get(
1817 '/download/issue%s_%s.diff' % (issue, patchset))
1818
1819 def GetIssueProperties(self):
1820 if self._props is None:
1821 issue = self.GetIssue()
1822 if not issue:
1823 self._props = {}
1824 else:
1825 self._props = self.RpcServer().get_issue_properties(issue, True)
1826 return self._props
1827
tandriie113dfd2016-10-11 10:20:12 -07001828 def CannotTriggerTryJobReason(self):
1829 props = self.GetIssueProperties()
1830 if not props:
1831 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1832 if props.get('closed'):
1833 return 'CL %s is closed' % self.GetIssue()
1834 if props.get('private'):
1835 return 'CL %s is private' % self.GetIssue()
1836 return None
1837
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838 def GetApprovingReviewers(self):
1839 return get_approving_reviewers(self.GetIssueProperties())
1840
tandriide281ae2016-10-12 06:02:30 -07001841 def GetIssueOwner(self):
1842 return (self.GetIssueProperties() or {}).get('owner_email')
1843
1844 def GetIssueProject(self):
1845 return (self.GetIssueProperties() or {}).get('project')
1846
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001847 def AddComment(self, message):
1848 return self.RpcServer().add_comment(self.GetIssue(), message)
1849
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001850 def GetStatus(self):
1851 """Apply a rough heuristic to give a simple summary of an issue's review
1852 or CQ status, assuming adherence to a common workflow.
1853
1854 Returns None if no issue for this branch, or one of the following keywords:
1855 * 'error' - error from review tool (including deleted issues)
1856 * 'unsent' - not sent for review
1857 * 'waiting' - waiting for review
1858 * 'reply' - waiting for owner to reply to review
1859 * 'lgtm' - LGTM from at least one approved reviewer
1860 * 'commit' - in the commit queue
1861 * 'closed' - closed
1862 """
1863 if not self.GetIssue():
1864 return None
1865
1866 try:
1867 props = self.GetIssueProperties()
1868 except urllib2.HTTPError:
1869 return 'error'
1870
1871 if props.get('closed'):
1872 # Issue is closed.
1873 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001874 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001875 # Issue is in the commit queue.
1876 return 'commit'
1877
1878 try:
1879 reviewers = self.GetApprovingReviewers()
1880 except urllib2.HTTPError:
1881 return 'error'
1882
1883 if reviewers:
1884 # Was LGTM'ed.
1885 return 'lgtm'
1886
1887 messages = props.get('messages') or []
1888
tandrii9d2c7a32016-06-22 03:42:45 -07001889 # Skip CQ messages that don't require owner's action.
1890 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1891 if 'Dry run:' in messages[-1]['text']:
1892 messages.pop()
1893 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1894 # This message always follows prior messages from CQ,
1895 # so skip this too.
1896 messages.pop()
1897 else:
1898 # This is probably a CQ messages warranting user attention.
1899 break
1900
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001901 if not messages:
1902 # No message was sent.
1903 return 'unsent'
1904 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001905 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001906 return 'reply'
1907 return 'waiting'
1908
dsansomee2d6fd92016-09-08 00:10:47 -07001909 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001910 return self.RpcServer().update_description(
1911 self.GetIssue(), self.description)
1912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001914 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001916 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001917 return self.SetFlags({flag: value})
1918
1919 def SetFlags(self, flags):
1920 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001921 """
phajdan.jr68598232016-08-10 03:28:28 -07001922 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001923 try:
tandrii4b233bd2016-07-06 03:50:29 -07001924 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001925 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001926 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001927 if e.code == 404:
1928 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1929 if e.code == 403:
1930 DieWithError(
1931 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001932 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001933 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001935 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 """Returns an upload.RpcServer() to access this review's rietveld instance.
1937 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001938 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001939 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001940 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001941 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001942 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001944 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001945 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001946 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947
tandrii5d48c322016-08-18 16:19:37 -07001948 @classmethod
1949 def PatchsetConfigKey(cls):
1950 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951
tandrii5d48c322016-08-18 16:19:37 -07001952 @classmethod
1953 def CodereviewServerConfigKey(cls):
1954 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001955
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 def GetRieveldObjForPresubmit(self):
1957 return self.RpcServer()
1958
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001959 def SetCQState(self, new_state):
1960 props = self.GetIssueProperties()
1961 if props.get('private'):
1962 DieWithError('Cannot set-commit on private issue')
1963
1964 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001965 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001966 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001967 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001968 else:
tandrii4b233bd2016-07-06 03:50:29 -07001969 assert new_state == _CQState.DRY_RUN
1970 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001971
1972
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001973 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1974 directory):
1975 # TODO(maruel): Use apply_issue.py
1976
1977 # PatchIssue should never be called with a dirty tree. It is up to the
1978 # caller to check this, but just in case we assert here since the
1979 # consequences of the caller not checking this could be dire.
1980 assert(not git_common.is_dirty_git_tree('apply'))
1981 assert(parsed_issue_arg.valid)
1982 self._changelist.issue = parsed_issue_arg.issue
1983 if parsed_issue_arg.hostname:
1984 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1985
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001986 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1987 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001988 assert parsed_issue_arg.patchset
1989 patchset = parsed_issue_arg.patchset
1990 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1991 else:
1992 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1993 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1994
1995 # Switch up to the top-level directory, if necessary, in preparation for
1996 # applying the patch.
1997 top = settings.GetRelativeRoot()
1998 if top:
1999 os.chdir(top)
2000
2001 # Git patches have a/ at the beginning of source paths. We strip that out
2002 # with a sed script rather than the -p flag to patch so we can feed either
2003 # Git or svn-style patches into the same apply command.
2004 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
2005 try:
2006 patch_data = subprocess2.check_output(
2007 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2008 except subprocess2.CalledProcessError:
2009 DieWithError('Git patch mungling failed.')
2010 logging.info(patch_data)
2011
2012 # We use "git apply" to apply the patch instead of "patch" so that we can
2013 # pick up file adds.
2014 # The --index flag means: also insert into the index (so we catch adds).
2015 cmd = ['git', 'apply', '--index', '-p0']
2016 if directory:
2017 cmd.extend(('--directory', directory))
2018 if reject:
2019 cmd.append('--reject')
2020 elif IsGitVersionAtLeast('1.7.12'):
2021 cmd.append('--3way')
2022 try:
2023 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
2024 stdin=patch_data, stdout=subprocess2.VOID)
2025 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07002026 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002027 return 1
2028
2029 # If we had an issue, commit the current state and register the issue.
2030 if not nocommit:
2031 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2032 'patch from issue %(i)s at patchset '
2033 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2034 % {'i': self.GetIssue(), 'p': patchset})])
2035 self.SetIssue(self.GetIssue())
2036 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002037 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002038 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002039 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002040 return 0
2041
2042 @staticmethod
2043 def ParseIssueURL(parsed_url):
2044 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2045 return None
wychen3c1c1722016-08-04 11:46:36 -07002046 # Rietveld patch: https://domain/<number>/#ps<patchset>
2047 match = re.match(r'/(\d+)/$', parsed_url.path)
2048 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2049 if match and match2:
2050 return _RietveldParsedIssueNumberArgument(
2051 issue=int(match.group(1)),
2052 patchset=int(match2.group(1)),
2053 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002054 # Typical url: https://domain/<issue_number>[/[other]]
2055 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2056 if match:
2057 return _RietveldParsedIssueNumberArgument(
2058 issue=int(match.group(1)),
2059 hostname=parsed_url.netloc)
2060 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2061 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2062 if match:
2063 return _RietveldParsedIssueNumberArgument(
2064 issue=int(match.group(1)),
2065 patchset=int(match.group(2)),
2066 hostname=parsed_url.netloc,
2067 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
2068 return None
2069
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002070 def CMDUploadChange(self, options, args, change):
2071 """Upload the patch to Rietveld."""
2072 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2073 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002074 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2075 if options.emulate_svn_auto_props:
2076 upload_args.append('--emulate_svn_auto_props')
2077
2078 change_desc = None
2079
2080 if options.email is not None:
2081 upload_args.extend(['--email', options.email])
2082
2083 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002084 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002085 upload_args.extend(['--title', options.title])
2086 if options.message:
2087 upload_args.extend(['--message', options.message])
2088 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002089 print('This branch is associated with issue %s. '
2090 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002091 else:
nodirca166002016-06-27 10:59:51 -07002092 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002093 upload_args.extend(['--title', options.title])
2094 message = (options.title or options.message or
2095 CreateDescriptionFromLog(args))
2096 change_desc = ChangeDescription(message)
2097 if options.reviewers or options.tbr_owners:
2098 change_desc.update_reviewers(options.reviewers,
2099 options.tbr_owners,
2100 change)
2101 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002102 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002103
2104 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002105 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002106 return 1
2107
2108 upload_args.extend(['--message', change_desc.description])
2109 if change_desc.get_reviewers():
2110 upload_args.append('--reviewers=%s' % ','.join(
2111 change_desc.get_reviewers()))
2112 if options.send_mail:
2113 if not change_desc.get_reviewers():
2114 DieWithError("Must specify reviewers to send email.")
2115 upload_args.append('--send_mail')
2116
2117 # We check this before applying rietveld.private assuming that in
2118 # rietveld.cc only addresses which we can send private CLs to are listed
2119 # if rietveld.private is set, and so we should ignore rietveld.cc only
2120 # when --private is specified explicitly on the command line.
2121 if options.private:
2122 logging.warn('rietveld.cc is ignored since private flag is specified. '
2123 'You need to review and add them manually if necessary.')
2124 cc = self.GetCCListWithoutDefault()
2125 else:
2126 cc = self.GetCCList()
2127 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002128 if change_desc.get_cced():
2129 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002130 if cc:
2131 upload_args.extend(['--cc', cc])
2132
2133 if options.private or settings.GetDefaultPrivateFlag() == "True":
2134 upload_args.append('--private')
2135
2136 upload_args.extend(['--git_similarity', str(options.similarity)])
2137 if not options.find_copies:
2138 upload_args.extend(['--git_no_find_copies'])
2139
2140 # Include the upstream repo's URL in the change -- this is useful for
2141 # projects that have their source spread across multiple repos.
2142 remote_url = self.GetGitBaseUrlFromConfig()
2143 if not remote_url:
2144 if settings.GetIsGitSvn():
2145 remote_url = self.GetGitSvnRemoteUrl()
2146 else:
2147 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2148 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2149 self.GetUpstreamBranch().split('/')[-1])
2150 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151 remote, remote_branch = self.GetRemoteBranch()
2152 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2153 settings.GetPendingRefPrefix())
2154 if target_ref:
2155 upload_args.extend(['--target_ref', target_ref])
2156
2157 # Look for dependent patchsets. See crbug.com/480453 for more details.
2158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2159 upstream_branch = ShortBranchName(upstream_branch)
2160 if remote is '.':
2161 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002162 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002163 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002164 print()
2165 print('Skipping dependency patchset upload because git config '
2166 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2167 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002168 else:
2169 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002170 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002171 auth_config=auth_config)
2172 branch_cl_issue_url = branch_cl.GetIssueURL()
2173 branch_cl_issue = branch_cl.GetIssue()
2174 branch_cl_patchset = branch_cl.GetPatchset()
2175 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2176 upload_args.extend(
2177 ['--depends_on_patchset', '%s:%s' % (
2178 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002179 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002180 '\n'
2181 'The current branch (%s) is tracking a local branch (%s) with '
2182 'an associated CL.\n'
2183 'Adding %s/#ps%s as a dependency patchset.\n'
2184 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2185 branch_cl_patchset))
2186
2187 project = settings.GetProject()
2188 if project:
2189 upload_args.extend(['--project', project])
2190
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 try:
2192 upload_args = ['upload'] + upload_args + args
2193 logging.info('upload.RealMain(%s)', upload_args)
2194 issue, patchset = upload.RealMain(upload_args)
2195 issue = int(issue)
2196 patchset = int(patchset)
2197 except KeyboardInterrupt:
2198 sys.exit(1)
2199 except:
2200 # If we got an exception after the user typed a description for their
2201 # change, back up the description before re-raising.
2202 if change_desc:
2203 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2204 print('\nGot exception while uploading -- saving description to %s\n' %
2205 backup_path)
2206 backup_file = open(backup_path, 'w')
2207 backup_file.write(change_desc.description)
2208 backup_file.close()
2209 raise
2210
2211 if not self.GetIssue():
2212 self.SetIssue(issue)
2213 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002214 return 0
2215
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002216
2217class _GerritChangelistImpl(_ChangelistCodereviewBase):
2218 def __init__(self, changelist, auth_config=None):
2219 # auth_config is Rietveld thing, kept here to preserve interface only.
2220 super(_GerritChangelistImpl, self).__init__(changelist)
2221 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002222 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002223 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002224 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002225
2226 def _GetGerritHost(self):
2227 # Lazy load of configs.
2228 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002229 if self._gerrit_host and '.' not in self._gerrit_host:
2230 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2231 # This happens for internal stuff http://crbug.com/614312.
2232 parsed = urlparse.urlparse(self.GetRemoteUrl())
2233 if parsed.scheme == 'sso':
2234 print('WARNING: using non https URLs for remote is likely broken\n'
2235 ' Your current remote is: %s' % self.GetRemoteUrl())
2236 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2237 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238 return self._gerrit_host
2239
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002240 def _GetGitHost(self):
2241 """Returns git host to be used when uploading change to Gerrit."""
2242 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2243
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002244 def GetCodereviewServer(self):
2245 if not self._gerrit_server:
2246 # If we're on a branch then get the server potentially associated
2247 # with that branch.
2248 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002249 self._gerrit_server = self._GitGetBranchConfigValue(
2250 self.CodereviewServerConfigKey())
2251 if self._gerrit_server:
2252 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002253 if not self._gerrit_server:
2254 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2255 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002256 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002257 parts[0] = parts[0] + '-review'
2258 self._gerrit_host = '.'.join(parts)
2259 self._gerrit_server = 'https://%s' % self._gerrit_host
2260 return self._gerrit_server
2261
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002262 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002263 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002264 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002265
tandrii5d48c322016-08-18 16:19:37 -07002266 @classmethod
2267 def PatchsetConfigKey(cls):
2268 return 'gerritpatchset'
2269
2270 @classmethod
2271 def CodereviewServerConfigKey(cls):
2272 return 'gerritserver'
2273
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002274 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002275 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002276 if settings.GetGerritSkipEnsureAuthenticated():
2277 # For projects with unusual authentication schemes.
2278 # See http://crbug.com/603378.
2279 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002280 # Lazy-loader to identify Gerrit and Git hosts.
2281 if gerrit_util.GceAuthenticator.is_gce():
2282 return
2283 self.GetCodereviewServer()
2284 git_host = self._GetGitHost()
2285 assert self._gerrit_server and self._gerrit_host
2286 cookie_auth = gerrit_util.CookiesAuthenticator()
2287
2288 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2289 git_auth = cookie_auth.get_auth_header(git_host)
2290 if gerrit_auth and git_auth:
2291 if gerrit_auth == git_auth:
2292 return
2293 print((
2294 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2295 ' Check your %s or %s file for credentials of hosts:\n'
2296 ' %s\n'
2297 ' %s\n'
2298 ' %s') %
2299 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2300 git_host, self._gerrit_host,
2301 cookie_auth.get_new_password_message(git_host)))
2302 if not force:
2303 ask_for_data('If you know what you are doing, press Enter to continue, '
2304 'Ctrl+C to abort.')
2305 return
2306 else:
2307 missing = (
2308 [] if gerrit_auth else [self._gerrit_host] +
2309 [] if git_auth else [git_host])
2310 DieWithError('Credentials for the following hosts are required:\n'
2311 ' %s\n'
2312 'These are read from %s (or legacy %s)\n'
2313 '%s' % (
2314 '\n '.join(missing),
2315 cookie_auth.get_gitcookies_path(),
2316 cookie_auth.get_netrc_path(),
2317 cookie_auth.get_new_password_message(git_host)))
2318
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002319 def _PostUnsetIssueProperties(self):
2320 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002321 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002322
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002323 def GetRieveldObjForPresubmit(self):
2324 class ThisIsNotRietveldIssue(object):
2325 def __nonzero__(self):
2326 # This is a hack to make presubmit_support think that rietveld is not
2327 # defined, yet still ensure that calls directly result in a decent
2328 # exception message below.
2329 return False
2330
2331 def __getattr__(self, attr):
2332 print(
2333 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2334 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2335 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2336 'or use Rietveld for codereview.\n'
2337 'See also http://crbug.com/579160.' % attr)
2338 raise NotImplementedError()
2339 return ThisIsNotRietveldIssue()
2340
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002341 def GetGerritObjForPresubmit(self):
2342 return presubmit_support.GerritAccessor(self._GetGerritHost())
2343
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002344 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002345 """Apply a rough heuristic to give a simple summary of an issue's review
2346 or CQ status, assuming adherence to a common workflow.
2347
2348 Returns None if no issue for this branch, or one of the following keywords:
2349 * 'error' - error from review tool (including deleted issues)
2350 * 'unsent' - no reviewers added
2351 * 'waiting' - waiting for review
2352 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002353 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2354 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002355 * 'commit' - in the commit queue
2356 * 'closed' - abandoned
2357 """
2358 if not self.GetIssue():
2359 return None
2360
2361 try:
2362 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002363 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002364 return 'error'
2365
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002366 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002367 return 'closed'
2368
2369 cq_label = data['labels'].get('Commit-Queue', {})
2370 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002371 votes = cq_label.get('all', [])
2372 highest_vote = 0
2373 for v in votes:
2374 highest_vote = max(highest_vote, v.get('value', 0))
2375 vote_value = str(highest_vote)
2376 if vote_value != '0':
2377 # Add a '+' if the value is not 0 to match the values in the label.
2378 # The cq_label does not have negatives.
2379 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002380 vote_text = cq_label.get('values', {}).get(vote_value, '')
2381 if vote_text.lower() == 'commit':
2382 return 'commit'
2383
2384 lgtm_label = data['labels'].get('Code-Review', {})
2385 if lgtm_label:
2386 if 'rejected' in lgtm_label:
2387 return 'not lgtm'
2388 if 'approved' in lgtm_label:
2389 return 'lgtm'
2390
2391 if not data.get('reviewers', {}).get('REVIEWER', []):
2392 return 'unsent'
2393
2394 messages = data.get('messages', [])
2395 if messages:
2396 owner = data['owner'].get('_account_id')
2397 last_message_author = messages[-1].get('author', {}).get('_account_id')
2398 if owner != last_message_author:
2399 # Some reply from non-owner.
2400 return 'reply'
2401
2402 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403
2404 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002405 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002406 return data['revisions'][data['current_revision']]['_number']
2407
2408 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002409 data = self._GetChangeDetail(['CURRENT_REVISION'])
2410 current_rev = data['current_revision']
2411 url = data['revisions'][current_rev]['fetch']['http']['url']
2412 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002413
dsansomee2d6fd92016-09-08 00:10:47 -07002414 def UpdateDescriptionRemote(self, description, force=False):
2415 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2416 if not force:
2417 ask_for_data(
2418 'The description cannot be modified while the issue has a pending '
2419 'unpublished edit. Either publish the edit in the Gerrit web UI '
2420 'or delete it.\n\n'
2421 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2422
2423 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2424 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002425 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2426 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002427
2428 def CloseIssue(self):
2429 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2430
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002431 def GetApprovingReviewers(self):
2432 """Returns a list of reviewers approving the change.
2433
2434 Note: not necessarily committers.
2435 """
2436 raise NotImplementedError()
2437
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002438 def SubmitIssue(self, wait_for_merge=True):
2439 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2440 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002442 def _GetChangeDetail(self, options=None, issue=None):
2443 options = options or []
2444 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002445 assert issue, 'issue is required to query Gerrit'
2446 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002447 options)
tandriic2405f52016-10-10 08:13:15 -07002448 if not data:
2449 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2450 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002451
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002452 def CMDLand(self, force, bypass_hooks, verbose):
2453 if git_common.is_dirty_git_tree('land'):
2454 return 1
tandriid60367b2016-06-22 05:25:12 -07002455 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2456 if u'Commit-Queue' in detail.get('labels', {}):
2457 if not force:
2458 ask_for_data('\nIt seems this repository has a Commit Queue, '
2459 'which can test and land changes for you. '
2460 'Are you sure you wish to bypass it?\n'
2461 'Press Enter to continue, Ctrl+C to abort.')
2462
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002463 differs = True
tandriic4344b52016-08-29 06:04:54 -07002464 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002465 # Note: git diff outputs nothing if there is no diff.
2466 if not last_upload or RunGit(['diff', last_upload]).strip():
2467 print('WARNING: some changes from local branch haven\'t been uploaded')
2468 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002469 if detail['current_revision'] == last_upload:
2470 differs = False
2471 else:
2472 print('WARNING: local branch contents differ from latest uploaded '
2473 'patchset')
2474 if differs:
2475 if not force:
2476 ask_for_data(
2477 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2478 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2479 elif not bypass_hooks:
2480 hook_results = self.RunHook(
2481 committing=True,
2482 may_prompt=not force,
2483 verbose=verbose,
2484 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2485 if not hook_results.should_continue():
2486 return 1
2487
2488 self.SubmitIssue(wait_for_merge=True)
2489 print('Issue %s has been submitted.' % self.GetIssueURL())
2490 return 0
2491
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002492 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2493 directory):
2494 assert not reject
2495 assert not nocommit
2496 assert not directory
2497 assert parsed_issue_arg.valid
2498
2499 self._changelist.issue = parsed_issue_arg.issue
2500
2501 if parsed_issue_arg.hostname:
2502 self._gerrit_host = parsed_issue_arg.hostname
2503 self._gerrit_server = 'https://%s' % self._gerrit_host
2504
tandriic2405f52016-10-10 08:13:15 -07002505 try:
2506 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2507 except GerritIssueNotExists as e:
2508 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002509
2510 if not parsed_issue_arg.patchset:
2511 # Use current revision by default.
2512 revision_info = detail['revisions'][detail['current_revision']]
2513 patchset = int(revision_info['_number'])
2514 else:
2515 patchset = parsed_issue_arg.patchset
2516 for revision_info in detail['revisions'].itervalues():
2517 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2518 break
2519 else:
2520 DieWithError('Couldn\'t find patchset %i in issue %i' %
2521 (parsed_issue_arg.patchset, self.GetIssue()))
2522
2523 fetch_info = revision_info['fetch']['http']
2524 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2525 RunGit(['cherry-pick', 'FETCH_HEAD'])
2526 self.SetIssue(self.GetIssue())
2527 self.SetPatchset(patchset)
2528 print('Committed patch for issue %i pathset %i locally' %
2529 (self.GetIssue(), self.GetPatchset()))
2530 return 0
2531
2532 @staticmethod
2533 def ParseIssueURL(parsed_url):
2534 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2535 return None
2536 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2537 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2538 # Short urls like https://domain/<issue_number> can be used, but don't allow
2539 # specifying the patchset (you'd 404), but we allow that here.
2540 if parsed_url.path == '/':
2541 part = parsed_url.fragment
2542 else:
2543 part = parsed_url.path
2544 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2545 if match:
2546 return _ParsedIssueNumberArgument(
2547 issue=int(match.group(2)),
2548 patchset=int(match.group(4)) if match.group(4) else None,
2549 hostname=parsed_url.netloc)
2550 return None
2551
tandrii16e0b4e2016-06-07 10:34:28 -07002552 def _GerritCommitMsgHookCheck(self, offer_removal):
2553 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2554 if not os.path.exists(hook):
2555 return
2556 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2557 # custom developer made one.
2558 data = gclient_utils.FileRead(hook)
2559 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2560 return
2561 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002562 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002563 'and may interfere with it in subtle ways.\n'
2564 'We recommend you remove the commit-msg hook.')
2565 if offer_removal:
2566 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2567 if reply.lower().startswith('y'):
2568 gclient_utils.rm_file_or_tree(hook)
2569 print('Gerrit commit-msg hook removed.')
2570 else:
2571 print('OK, will keep Gerrit commit-msg hook in place.')
2572
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002573 def CMDUploadChange(self, options, args, change):
2574 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002575 if options.squash and options.no_squash:
2576 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002577
2578 if not options.squash and not options.no_squash:
2579 # Load default for user, repo, squash=true, in this order.
2580 options.squash = settings.GetSquashGerritUploads()
2581 elif options.no_squash:
2582 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002583
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002584 # We assume the remote called "origin" is the one we want.
2585 # It is probably not worthwhile to support different workflows.
2586 gerrit_remote = 'origin'
2587
2588 remote, remote_branch = self.GetRemoteBranch()
2589 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2590 pending_prefix='')
2591
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002592 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002593 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002594 if self.GetIssue():
2595 # Try to get the message from a previous upload.
2596 message = self.GetDescription()
2597 if not message:
2598 DieWithError(
2599 'failed to fetch description from current Gerrit issue %d\n'
2600 '%s' % (self.GetIssue(), self.GetIssueURL()))
2601 change_id = self._GetChangeDetail()['change_id']
2602 while True:
2603 footer_change_ids = git_footers.get_footer_change_id(message)
2604 if footer_change_ids == [change_id]:
2605 break
2606 if not footer_change_ids:
2607 message = git_footers.add_footer_change_id(message, change_id)
2608 print('WARNING: appended missing Change-Id to issue description')
2609 continue
2610 # There is already a valid footer but with different or several ids.
2611 # Doing this automatically is non-trivial as we don't want to lose
2612 # existing other footers, yet we want to append just 1 desired
2613 # Change-Id. Thus, just create a new footer, but let user verify the
2614 # new description.
2615 message = '%s\n\nChange-Id: %s' % (message, change_id)
2616 print(
2617 'WARNING: issue %s has Change-Id footer(s):\n'
2618 ' %s\n'
2619 'but issue has Change-Id %s, according to Gerrit.\n'
2620 'Please, check the proposed correction to the description, '
2621 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2622 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2623 change_id))
2624 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2625 if not options.force:
2626 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002627 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002628 message = change_desc.description
2629 if not message:
2630 DieWithError("Description is empty. Aborting...")
2631 # Continue the while loop.
2632 # Sanity check of this code - we should end up with proper message
2633 # footer.
2634 assert [change_id] == git_footers.get_footer_change_id(message)
2635 change_desc = ChangeDescription(message)
2636 else:
2637 change_desc = ChangeDescription(
2638 options.message or CreateDescriptionFromLog(args))
2639 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002640 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 if not change_desc.description:
2642 DieWithError("Description is empty. Aborting...")
2643 message = change_desc.description
2644 change_ids = git_footers.get_footer_change_id(message)
2645 if len(change_ids) > 1:
2646 DieWithError('too many Change-Id footers, at most 1 allowed.')
2647 if not change_ids:
2648 # Generate the Change-Id automatically.
2649 message = git_footers.add_footer_change_id(
2650 message, GenerateGerritChangeId(message))
2651 change_desc.set_description(message)
2652 change_ids = git_footers.get_footer_change_id(message)
2653 assert len(change_ids) == 1
2654 change_id = change_ids[0]
2655
2656 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2657 if remote is '.':
2658 # If our upstream branch is local, we base our squashed commit on its
2659 # squashed version.
2660 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2661 # Check the squashed hash of the parent.
2662 parent = RunGit(['config',
2663 'branch.%s.gerritsquashhash' % upstream_branch_name],
2664 error_ok=True).strip()
2665 # Verify that the upstream branch has been uploaded too, otherwise
2666 # Gerrit will create additional CLs when uploading.
2667 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2668 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002669 DieWithError(
2670 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002671 'Note: maybe you\'ve uploaded it with --no-squash. '
2672 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 ' git cl upload --squash\n' % upstream_branch_name)
2674 else:
2675 parent = self.GetCommonAncestorWithUpstream()
2676
2677 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2678 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2679 '-m', message]).strip()
2680 else:
2681 change_desc = ChangeDescription(
2682 options.message or CreateDescriptionFromLog(args))
2683 if not change_desc.description:
2684 DieWithError("Description is empty. Aborting...")
2685
2686 if not git_footers.get_footer_change_id(change_desc.description):
2687 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002688 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2689 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002690 ref_to_push = 'HEAD'
2691 parent = '%s/%s' % (gerrit_remote, branch)
2692 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2693
2694 assert change_desc
2695 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2696 ref_to_push)]).splitlines()
2697 if len(commits) > 1:
2698 print('WARNING: This will upload %d commits. Run the following command '
2699 'to see which commits will be uploaded: ' % len(commits))
2700 print('git log %s..%s' % (parent, ref_to_push))
2701 print('You can also use `git squash-branch` to squash these into a '
2702 'single commit.')
2703 ask_for_data('About to upload; enter to confirm.')
2704
2705 if options.reviewers or options.tbr_owners:
2706 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2707 change)
2708
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002709 # Extra options that can be specified at push time. Doc:
2710 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2711 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002712 if change_desc.get_reviewers(tbr_only=True):
2713 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2714 refspec_opts.append('l=Code-Review+1')
2715
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002716 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002717 if not re.match(r'^[\w ]+$', options.title):
2718 options.title = re.sub(r'[^\w ]', '', options.title)
2719 print('WARNING: Patchset title may only contain alphanumeric chars '
2720 'and spaces. Cleaned up title:\n%s' % options.title)
2721 if not options.force:
2722 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002723 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2724 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002725 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2726
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002727 if options.send_mail:
2728 if not change_desc.get_reviewers():
2729 DieWithError('Must specify reviewers to send email.')
2730 refspec_opts.append('notify=ALL')
2731 else:
2732 refspec_opts.append('notify=NONE')
2733
tandrii99a72f22016-08-17 14:33:24 -07002734 reviewers = change_desc.get_reviewers()
2735 if reviewers:
2736 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002737
agablec6787972016-09-09 16:13:34 -07002738 if options.private:
2739 refspec_opts.append('draft')
2740
rmistry9eadede2016-09-19 11:22:43 -07002741 if options.topic:
2742 # Documentation on Gerrit topics is here:
2743 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2744 refspec_opts.append('topic=%s' % options.topic)
2745
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002746 refspec_suffix = ''
2747 if refspec_opts:
2748 refspec_suffix = '%' + ','.join(refspec_opts)
2749 assert ' ' not in refspec_suffix, (
2750 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002751 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002752
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002753 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002754 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002755 print_stdout=True,
2756 # Flush after every line: useful for seeing progress when running as
2757 # recipe.
2758 filter_fn=lambda _: sys.stdout.flush())
2759
2760 if options.squash:
2761 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2762 change_numbers = [m.group(1)
2763 for m in map(regex.match, push_stdout.splitlines())
2764 if m]
2765 if len(change_numbers) != 1:
2766 DieWithError(
2767 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2768 'Change-Id: %s') % (len(change_numbers), change_id))
2769 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002770 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002771
2772 # Add cc's from the CC_LIST and --cc flag (if any).
2773 cc = self.GetCCList().split(',')
2774 if options.cc:
2775 cc.extend(options.cc)
2776 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002777 if change_desc.get_cced():
2778 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002779 if cc:
2780 gerrit_util.AddReviewers(
2781 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2782
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002783 return 0
2784
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002785 def _AddChangeIdToCommitMessage(self, options, args):
2786 """Re-commits using the current message, assumes the commit hook is in
2787 place.
2788 """
2789 log_desc = options.message or CreateDescriptionFromLog(args)
2790 git_command = ['commit', '--amend', '-m', log_desc]
2791 RunGit(git_command)
2792 new_log_desc = CreateDescriptionFromLog(args)
2793 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002794 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002795 return new_log_desc
2796 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002797 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002798
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002799 def SetCQState(self, new_state):
2800 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002801 vote_map = {
2802 _CQState.NONE: 0,
2803 _CQState.DRY_RUN: 1,
2804 _CQState.COMMIT : 2,
2805 }
2806 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2807 labels={'Commit-Queue': vote_map[new_state]})
2808
tandriie113dfd2016-10-11 10:20:12 -07002809 def CannotTriggerTryJobReason(self):
2810 # TODO(tandrii): implement for Gerrit.
2811 raise NotImplementedError()
2812
tandriide281ae2016-10-12 06:02:30 -07002813 def GetIssueOwner(self):
2814 # TODO(tandrii): implement for Gerrit.
2815 raise NotImplementedError()
2816
2817 def GetIssueProject(self):
2818 # TODO(tandrii): implement for Gerrit.
2819 raise NotImplementedError()
2820
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002821
2822_CODEREVIEW_IMPLEMENTATIONS = {
2823 'rietveld': _RietveldChangelistImpl,
2824 'gerrit': _GerritChangelistImpl,
2825}
2826
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002827
iannuccie53c9352016-08-17 14:40:40 -07002828def _add_codereview_issue_select_options(parser, extra=""):
2829 _add_codereview_select_options(parser)
2830
2831 text = ('Operate on this issue number instead of the current branch\'s '
2832 'implicit issue.')
2833 if extra:
2834 text += ' '+extra
2835 parser.add_option('-i', '--issue', type=int, help=text)
2836
2837
2838def _process_codereview_issue_select_options(parser, options):
2839 _process_codereview_select_options(parser, options)
2840 if options.issue is not None and not options.forced_codereview:
2841 parser.error('--issue must be specified with either --rietveld or --gerrit')
2842
2843
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002844def _add_codereview_select_options(parser):
2845 """Appends --gerrit and --rietveld options to force specific codereview."""
2846 parser.codereview_group = optparse.OptionGroup(
2847 parser, 'EXPERIMENTAL! Codereview override options')
2848 parser.add_option_group(parser.codereview_group)
2849 parser.codereview_group.add_option(
2850 '--gerrit', action='store_true',
2851 help='Force the use of Gerrit for codereview')
2852 parser.codereview_group.add_option(
2853 '--rietveld', action='store_true',
2854 help='Force the use of Rietveld for codereview')
2855
2856
2857def _process_codereview_select_options(parser, options):
2858 if options.gerrit and options.rietveld:
2859 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2860 options.forced_codereview = None
2861 if options.gerrit:
2862 options.forced_codereview = 'gerrit'
2863 elif options.rietveld:
2864 options.forced_codereview = 'rietveld'
2865
2866
tandriif9aefb72016-07-01 09:06:51 -07002867def _get_bug_line_values(default_project, bugs):
2868 """Given default_project and comma separated list of bugs, yields bug line
2869 values.
2870
2871 Each bug can be either:
2872 * a number, which is combined with default_project
2873 * string, which is left as is.
2874
2875 This function may produce more than one line, because bugdroid expects one
2876 project per line.
2877
2878 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2879 ['v8:123', 'chromium:789']
2880 """
2881 default_bugs = []
2882 others = []
2883 for bug in bugs.split(','):
2884 bug = bug.strip()
2885 if bug:
2886 try:
2887 default_bugs.append(int(bug))
2888 except ValueError:
2889 others.append(bug)
2890
2891 if default_bugs:
2892 default_bugs = ','.join(map(str, default_bugs))
2893 if default_project:
2894 yield '%s:%s' % (default_project, default_bugs)
2895 else:
2896 yield default_bugs
2897 for other in sorted(others):
2898 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2899 yield other
2900
2901
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002902class ChangeDescription(object):
2903 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002904 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002905 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002906 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002907
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002908 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002909 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002910
agable@chromium.org42c20792013-09-12 17:34:49 +00002911 @property # www.logilab.org/ticket/89786
2912 def description(self): # pylint: disable=E0202
2913 return '\n'.join(self._description_lines)
2914
2915 def set_description(self, desc):
2916 if isinstance(desc, basestring):
2917 lines = desc.splitlines()
2918 else:
2919 lines = [line.rstrip() for line in desc]
2920 while lines and not lines[0]:
2921 lines.pop(0)
2922 while lines and not lines[-1]:
2923 lines.pop(-1)
2924 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002925
piman@chromium.org336f9122014-09-04 02:16:55 +00002926 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002927 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002928 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002929 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002930 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002931 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002932
agable@chromium.org42c20792013-09-12 17:34:49 +00002933 # Get the set of R= and TBR= lines and remove them from the desciption.
2934 regexp = re.compile(self.R_LINE)
2935 matches = [regexp.match(line) for line in self._description_lines]
2936 new_desc = [l for i, l in enumerate(self._description_lines)
2937 if not matches[i]]
2938 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002939
agable@chromium.org42c20792013-09-12 17:34:49 +00002940 # Construct new unified R= and TBR= lines.
2941 r_names = []
2942 tbr_names = []
2943 for match in matches:
2944 if not match:
2945 continue
2946 people = cleanup_list([match.group(2).strip()])
2947 if match.group(1) == 'TBR':
2948 tbr_names.extend(people)
2949 else:
2950 r_names.extend(people)
2951 for name in r_names:
2952 if name not in reviewers:
2953 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002954 if add_owners_tbr:
2955 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002956 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002957 all_reviewers = set(tbr_names + reviewers)
2958 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2959 all_reviewers)
2960 tbr_names.extend(owners_db.reviewers_for(missing_files,
2961 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002962 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2963 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2964
2965 # Put the new lines in the description where the old first R= line was.
2966 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2967 if 0 <= line_loc < len(self._description_lines):
2968 if new_tbr_line:
2969 self._description_lines.insert(line_loc, new_tbr_line)
2970 if new_r_line:
2971 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002972 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002973 if new_r_line:
2974 self.append_footer(new_r_line)
2975 if new_tbr_line:
2976 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002977
tandriif9aefb72016-07-01 09:06:51 -07002978 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002979 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002980 self.set_description([
2981 '# Enter a description of the change.',
2982 '# This will be displayed on the codereview site.',
2983 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002984 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002985 '--------------------',
2986 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002987
agable@chromium.org42c20792013-09-12 17:34:49 +00002988 regexp = re.compile(self.BUG_LINE)
2989 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002990 prefix = settings.GetBugPrefix()
2991 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2992 for value in values:
2993 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2994 self.append_footer('BUG=%s' % value)
2995
agable@chromium.org42c20792013-09-12 17:34:49 +00002996 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002997 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002998 if not content:
2999 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003000 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003001
3002 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003003 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3004 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003005 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003007
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003008 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003009 """Adds a footer line to the description.
3010
3011 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3012 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3013 that Gerrit footers are always at the end.
3014 """
3015 parsed_footer_line = git_footers.parse_footer(line)
3016 if parsed_footer_line:
3017 # Line is a gerrit footer in the form: Footer-Key: any value.
3018 # Thus, must be appended observing Gerrit footer rules.
3019 self.set_description(
3020 git_footers.add_footer(self.description,
3021 key=parsed_footer_line[0],
3022 value=parsed_footer_line[1]))
3023 return
3024
3025 if not self._description_lines:
3026 self._description_lines.append(line)
3027 return
3028
3029 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3030 if gerrit_footers:
3031 # git_footers.split_footers ensures that there is an empty line before
3032 # actual (gerrit) footers, if any. We have to keep it that way.
3033 assert top_lines and top_lines[-1] == ''
3034 top_lines, separator = top_lines[:-1], top_lines[-1:]
3035 else:
3036 separator = [] # No need for separator if there are no gerrit_footers.
3037
3038 prev_line = top_lines[-1] if top_lines else ''
3039 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3040 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3041 top_lines.append('')
3042 top_lines.append(line)
3043 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003044
tandrii99a72f22016-08-17 14:33:24 -07003045 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003046 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003047 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003048 reviewers = [match.group(2).strip()
3049 for match in matches
3050 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003051 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003052
bradnelsond975b302016-10-23 12:20:23 -07003053 def get_cced(self):
3054 """Retrieves the list of reviewers."""
3055 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3056 cced = [match.group(2).strip() for match in matches if match]
3057 return cleanup_list(cced)
3058
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003059
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003060def get_approving_reviewers(props):
3061 """Retrieves the reviewers that approved a CL from the issue properties with
3062 messages.
3063
3064 Note that the list may contain reviewers that are not committer, thus are not
3065 considered by the CQ.
3066 """
3067 return sorted(
3068 set(
3069 message['sender']
3070 for message in props['messages']
3071 if message['approval'] and message['sender'] in props['reviewers']
3072 )
3073 )
3074
3075
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003076def FindCodereviewSettingsFile(filename='codereview.settings'):
3077 """Finds the given file starting in the cwd and going up.
3078
3079 Only looks up to the top of the repository unless an
3080 'inherit-review-settings-ok' file exists in the root of the repository.
3081 """
3082 inherit_ok_file = 'inherit-review-settings-ok'
3083 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003084 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003085 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3086 root = '/'
3087 while True:
3088 if filename in os.listdir(cwd):
3089 if os.path.isfile(os.path.join(cwd, filename)):
3090 return open(os.path.join(cwd, filename))
3091 if cwd == root:
3092 break
3093 cwd = os.path.dirname(cwd)
3094
3095
3096def LoadCodereviewSettingsFromFile(fileobj):
3097 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003098 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003099
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003100 def SetProperty(name, setting, unset_error_ok=False):
3101 fullname = 'rietveld.' + name
3102 if setting in keyvals:
3103 RunGit(['config', fullname, keyvals[setting]])
3104 else:
3105 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3106
tandrii48df5812016-10-17 03:55:37 -07003107 if not keyvals.get('GERRIT_HOST', False):
3108 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003109 # Only server setting is required. Other settings can be absent.
3110 # In that case, we ignore errors raised during option deletion attempt.
3111 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003112 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003113 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3114 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003115 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003116 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003117 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3118 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003119 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003120 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003121 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003122 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003123 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3124 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003125
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003126 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003127 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003128
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003129 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003130 RunGit(['config', 'gerrit.squash-uploads',
3131 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003132
tandrii@chromium.org28253532016-04-14 13:46:56 +00003133 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003134 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003135 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003137 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3138 #should be of the form
3139 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3140 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3141 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3142 keyvals['ORIGIN_URL_CONFIG']])
3143
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003144
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003145def urlretrieve(source, destination):
3146 """urllib is broken for SSL connections via a proxy therefore we
3147 can't use urllib.urlretrieve()."""
3148 with open(destination, 'w') as f:
3149 f.write(urllib2.urlopen(source).read())
3150
3151
ukai@chromium.org712d6102013-11-27 00:52:58 +00003152def hasSheBang(fname):
3153 """Checks fname is a #! script."""
3154 with open(fname) as f:
3155 return f.read(2).startswith('#!')
3156
3157
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003158# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3159def DownloadHooks(*args, **kwargs):
3160 pass
3161
3162
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003163def DownloadGerritHook(force):
3164 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003165
3166 Args:
3167 force: True to update hooks. False to install hooks if not present.
3168 """
3169 if not settings.GetIsGerrit():
3170 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003171 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003172 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3173 if not os.access(dst, os.X_OK):
3174 if os.path.exists(dst):
3175 if not force:
3176 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003177 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003178 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003179 if not hasSheBang(dst):
3180 DieWithError('Not a script: %s\n'
3181 'You need to download from\n%s\n'
3182 'into .git/hooks/commit-msg and '
3183 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003184 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3185 except Exception:
3186 if os.path.exists(dst):
3187 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003188 DieWithError('\nFailed to download hooks.\n'
3189 'You need to download from\n%s\n'
3190 'into .git/hooks/commit-msg and '
3191 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003192
3193
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003194
3195def GetRietveldCodereviewSettingsInteractively():
3196 """Prompt the user for settings."""
3197 server = settings.GetDefaultServerUrl(error_ok=True)
3198 prompt = 'Rietveld server (host[:port])'
3199 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3200 newserver = ask_for_data(prompt + ':')
3201 if not server and not newserver:
3202 newserver = DEFAULT_SERVER
3203 if newserver:
3204 newserver = gclient_utils.UpgradeToHttps(newserver)
3205 if newserver != server:
3206 RunGit(['config', 'rietveld.server', newserver])
3207
3208 def SetProperty(initial, caption, name, is_url):
3209 prompt = caption
3210 if initial:
3211 prompt += ' ("x" to clear) [%s]' % initial
3212 new_val = ask_for_data(prompt + ':')
3213 if new_val == 'x':
3214 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3215 elif new_val:
3216 if is_url:
3217 new_val = gclient_utils.UpgradeToHttps(new_val)
3218 if new_val != initial:
3219 RunGit(['config', 'rietveld.' + name, new_val])
3220
3221 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3222 SetProperty(settings.GetDefaultPrivateFlag(),
3223 'Private flag (rietveld only)', 'private', False)
3224 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3225 'tree-status-url', False)
3226 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3227 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3228 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3229 'run-post-upload-hook', False)
3230
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003231@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003233 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003234
tandrii5d0a0422016-09-14 06:24:35 -07003235 print('WARNING: git cl config works for Rietveld only')
3236 # TODO(tandrii): remove this once we switch to Gerrit.
3237 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003238 parser.add_option('--activate-update', action='store_true',
3239 help='activate auto-updating [rietveld] section in '
3240 '.git/config')
3241 parser.add_option('--deactivate-update', action='store_true',
3242 help='deactivate auto-updating [rietveld] section in '
3243 '.git/config')
3244 options, args = parser.parse_args(args)
3245
3246 if options.deactivate_update:
3247 RunGit(['config', 'rietveld.autoupdate', 'false'])
3248 return
3249
3250 if options.activate_update:
3251 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3252 return
3253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003255 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 return 0
3257
3258 url = args[0]
3259 if not url.endswith('codereview.settings'):
3260 url = os.path.join(url, 'codereview.settings')
3261
3262 # Load code review settings and download hooks (if available).
3263 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3264 return 0
3265
3266
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003267def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003268 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003269 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3270 branch = ShortBranchName(branchref)
3271 _, args = parser.parse_args(args)
3272 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003273 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003274 return RunGit(['config', 'branch.%s.base-url' % branch],
3275 error_ok=False).strip()
3276 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003277 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003278 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3279 error_ok=False).strip()
3280
3281
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003282def color_for_status(status):
3283 """Maps a Changelist status to color, for CMDstatus and other tools."""
3284 return {
3285 'unsent': Fore.RED,
3286 'waiting': Fore.BLUE,
3287 'reply': Fore.YELLOW,
3288 'lgtm': Fore.GREEN,
3289 'commit': Fore.MAGENTA,
3290 'closed': Fore.CYAN,
3291 'error': Fore.WHITE,
3292 }.get(status, Fore.WHITE)
3293
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003294
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003295def get_cl_statuses(changes, fine_grained, max_processes=None):
3296 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003297
3298 If fine_grained is true, this will fetch CL statuses from the server.
3299 Otherwise, simply indicate if there's a matching url for the given branches.
3300
3301 If max_processes is specified, it is used as the maximum number of processes
3302 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3303 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003304
3305 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003306 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003307 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003308 upload.verbosity = 0
3309
3310 if fine_grained:
3311 # Process one branch synchronously to work through authentication, then
3312 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003313 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003314 def fetch(cl):
3315 try:
3316 return (cl, cl.GetStatus())
3317 except:
3318 # See http://crbug.com/629863.
3319 logging.exception('failed to fetch status for %s:', cl)
3320 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003321 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003322
tandriiea9514a2016-08-17 12:32:37 -07003323 changes_to_fetch = changes[1:]
3324 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003325 # Exit early if there was only one branch to fetch.
3326 return
3327
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003328 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003329 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003330 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003331 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003332
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003333 fetched_cls = set()
3334 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003335 while True:
3336 try:
3337 row = it.next(timeout=5)
3338 except multiprocessing.TimeoutError:
3339 break
3340
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003341 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003342 yield row
3343
3344 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003345 for cl in set(changes_to_fetch) - fetched_cls:
3346 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003347
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003348 else:
3349 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003350 for cl in changes:
3351 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003352
rmistry@google.com2dd99862015-06-22 12:22:18 +00003353
3354def upload_branch_deps(cl, args):
3355 """Uploads CLs of local branches that are dependents of the current branch.
3356
3357 If the local branch dependency tree looks like:
3358 test1 -> test2.1 -> test3.1
3359 -> test3.2
3360 -> test2.2 -> test3.3
3361
3362 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3363 run on the dependent branches in this order:
3364 test2.1, test3.1, test3.2, test2.2, test3.3
3365
3366 Note: This function does not rebase your local dependent branches. Use it when
3367 you make a change to the parent branch that will not conflict with its
3368 dependent branches, and you would like their dependencies updated in
3369 Rietveld.
3370 """
3371 if git_common.is_dirty_git_tree('upload-branch-deps'):
3372 return 1
3373
3374 root_branch = cl.GetBranch()
3375 if root_branch is None:
3376 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3377 'Get on a branch!')
3378 if not cl.GetIssue() or not cl.GetPatchset():
3379 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3380 'patchset dependencies without an uploaded CL.')
3381
3382 branches = RunGit(['for-each-ref',
3383 '--format=%(refname:short) %(upstream:short)',
3384 'refs/heads'])
3385 if not branches:
3386 print('No local branches found.')
3387 return 0
3388
3389 # Create a dictionary of all local branches to the branches that are dependent
3390 # on it.
3391 tracked_to_dependents = collections.defaultdict(list)
3392 for b in branches.splitlines():
3393 tokens = b.split()
3394 if len(tokens) == 2:
3395 branch_name, tracked = tokens
3396 tracked_to_dependents[tracked].append(branch_name)
3397
vapiera7fbd5a2016-06-16 09:17:49 -07003398 print()
3399 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003400 dependents = []
3401 def traverse_dependents_preorder(branch, padding=''):
3402 dependents_to_process = tracked_to_dependents.get(branch, [])
3403 padding += ' '
3404 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003405 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003406 dependents.append(dependent)
3407 traverse_dependents_preorder(dependent, padding)
3408 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003409 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003410
3411 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003412 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003413 return 0
3414
vapiera7fbd5a2016-06-16 09:17:49 -07003415 print('This command will checkout all dependent branches and run '
3416 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003417 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3418
andybons@chromium.org962f9462016-02-03 20:00:42 +00003419 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003420 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003421 args.extend(['-t', 'Updated patchset dependency'])
3422
rmistry@google.com2dd99862015-06-22 12:22:18 +00003423 # Record all dependents that failed to upload.
3424 failures = {}
3425 # Go through all dependents, checkout the branch and upload.
3426 try:
3427 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003428 print()
3429 print('--------------------------------------')
3430 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003431 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003432 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003433 try:
3434 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003435 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003436 failures[dependent_branch] = 1
3437 except: # pylint: disable=W0702
3438 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003439 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003440 finally:
3441 # Swap back to the original root branch.
3442 RunGit(['checkout', '-q', root_branch])
3443
vapiera7fbd5a2016-06-16 09:17:49 -07003444 print()
3445 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003446 for dependent_branch in dependents:
3447 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003448 print(' %s : %s' % (dependent_branch, upload_status))
3449 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003450
3451 return 0
3452
3453
kmarshall3bff56b2016-06-06 18:31:47 -07003454def CMDarchive(parser, args):
3455 """Archives and deletes branches associated with closed changelists."""
3456 parser.add_option(
3457 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003458 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003459 parser.add_option(
3460 '-f', '--force', action='store_true',
3461 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003462 parser.add_option(
3463 '-d', '--dry-run', action='store_true',
3464 help='Skip the branch tagging and removal steps.')
3465 parser.add_option(
3466 '-t', '--notags', action='store_true',
3467 help='Do not tag archived branches. '
3468 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003469
3470 auth.add_auth_options(parser)
3471 options, args = parser.parse_args(args)
3472 if args:
3473 parser.error('Unsupported args: %s' % ' '.join(args))
3474 auth_config = auth.extract_auth_config_from_options(options)
3475
3476 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3477 if not branches:
3478 return 0
3479
vapiera7fbd5a2016-06-16 09:17:49 -07003480 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003481 changes = [Changelist(branchref=b, auth_config=auth_config)
3482 for b in branches.splitlines()]
3483 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3484 statuses = get_cl_statuses(changes,
3485 fine_grained=True,
3486 max_processes=options.maxjobs)
3487 proposal = [(cl.GetBranch(),
3488 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3489 for cl, status in statuses
3490 if status == 'closed']
3491 proposal.sort()
3492
3493 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003494 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003495 return 0
3496
3497 current_branch = GetCurrentBranch()
3498
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003500 if options.notags:
3501 for next_item in proposal:
3502 print(' ' + next_item[0])
3503 else:
3504 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3505 for next_item in proposal:
3506 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003507
kmarshall9249e012016-08-23 12:02:16 -07003508 # Quit now on precondition failure or if instructed by the user, either
3509 # via an interactive prompt or by command line flags.
3510 if options.dry_run:
3511 print('\nNo changes were made (dry run).\n')
3512 return 0
3513 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003514 print('You are currently on a branch \'%s\' which is associated with a '
3515 'closed codereview issue, so archive cannot proceed. Please '
3516 'checkout another branch and run this command again.' %
3517 current_branch)
3518 return 1
kmarshall9249e012016-08-23 12:02:16 -07003519 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003520 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3521 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003522 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003523 return 1
3524
3525 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003526 if not options.notags:
3527 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003528 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003529
vapiera7fbd5a2016-06-16 09:17:49 -07003530 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003531
3532 return 0
3533
3534
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003535def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003536 """Show status of changelists.
3537
3538 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003539 - Red not sent for review or broken
3540 - Blue waiting for review
3541 - Yellow waiting for you to reply to review
3542 - Green LGTM'ed
3543 - Magenta in the commit queue
3544 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003545
3546 Also see 'git cl comments'.
3547 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003549 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003550 parser.add_option('-f', '--fast', action='store_true',
3551 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003552 parser.add_option(
3553 '-j', '--maxjobs', action='store', type=int,
3554 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003555
3556 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003557 _add_codereview_issue_select_options(
3558 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003559 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003560 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003561 if args:
3562 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003563 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564
iannuccie53c9352016-08-17 14:40:40 -07003565 if options.issue is not None and not options.field:
3566 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003567
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003568 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003569 cl = Changelist(auth_config=auth_config, issue=options.issue,
3570 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003571 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003573 elif options.field == 'id':
3574 issueid = cl.GetIssue()
3575 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003576 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003577 elif options.field == 'patch':
3578 patchset = cl.GetPatchset()
3579 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003580 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003581 elif options.field == 'status':
3582 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583 elif options.field == 'url':
3584 url = cl.GetIssueURL()
3585 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003587 return 0
3588
3589 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3590 if not branches:
3591 print('No local branch found.')
3592 return 0
3593
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003594 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003595 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003596 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003597 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003598 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003599 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003600 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003601
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003602 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003603 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3604 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3605 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003606 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003607 c, status = output.next()
3608 branch_statuses[c.GetBranch()] = status
3609 status = branch_statuses.pop(branch)
3610 url = cl.GetIssueURL()
3611 if url and (not status or status == 'error'):
3612 # The issue probably doesn't exist anymore.
3613 url += ' (broken)'
3614
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003615 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003616 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003617 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003618 color = ''
3619 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003620 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003621 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003622 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003623 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003624
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003625 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print()
3627 print('Current branch:',)
3628 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003629 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003630 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003631 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003632 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003633 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003634 print('Issue description:')
3635 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636 return 0
3637
3638
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003639def colorize_CMDstatus_doc():
3640 """To be called once in main() to add colors to git cl status help."""
3641 colors = [i for i in dir(Fore) if i[0].isupper()]
3642
3643 def colorize_line(line):
3644 for color in colors:
3645 if color in line.upper():
3646 # Extract whitespaces first and the leading '-'.
3647 indent = len(line) - len(line.lstrip(' ')) + 1
3648 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3649 return line
3650
3651 lines = CMDstatus.__doc__.splitlines()
3652 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3653
3654
phajdan.jre328cf92016-08-22 04:12:17 -07003655def write_json(path, contents):
3656 with open(path, 'w') as f:
3657 json.dump(contents, f)
3658
3659
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003660@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003662 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663
3664 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003665 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003666 parser.add_option('-r', '--reverse', action='store_true',
3667 help='Lookup the branch(es) for the specified issues. If '
3668 'no issues are specified, all branches with mapped '
3669 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003670 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003671 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003672 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003673 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003674
dnj@chromium.org406c4402015-03-03 17:22:28 +00003675 if options.reverse:
3676 branches = RunGit(['for-each-ref', 'refs/heads',
3677 '--format=%(refname:short)']).splitlines()
3678
3679 # Reverse issue lookup.
3680 issue_branch_map = {}
3681 for branch in branches:
3682 cl = Changelist(branchref=branch)
3683 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3684 if not args:
3685 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003686 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003687 for issue in args:
3688 if not issue:
3689 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003690 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003691 print('Branch for issue number %s: %s' % (
3692 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003693 if options.json:
3694 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003695 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003696 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003697 if len(args) > 0:
3698 try:
3699 issue = int(args[0])
3700 except ValueError:
3701 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003702 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003703 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003704 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003705 if options.json:
3706 write_json(options.json, {
3707 'issue': cl.GetIssue(),
3708 'issue_url': cl.GetIssueURL(),
3709 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710 return 0
3711
3712
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003713def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003714 """Shows or posts review comments for any changelist."""
3715 parser.add_option('-a', '--add-comment', dest='comment',
3716 help='comment to add to an issue')
3717 parser.add_option('-i', dest='issue',
3718 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003719 parser.add_option('-j', '--json-file',
3720 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003721 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003722 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003723 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003724
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003725 issue = None
3726 if options.issue:
3727 try:
3728 issue = int(options.issue)
3729 except ValueError:
3730 DieWithError('A review issue id is expected to be a number')
3731
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003732 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003733
3734 if options.comment:
3735 cl.AddComment(options.comment)
3736 return 0
3737
3738 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003739 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003740 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003741 summary.append({
3742 'date': message['date'],
3743 'lgtm': False,
3744 'message': message['text'],
3745 'not_lgtm': False,
3746 'sender': message['sender'],
3747 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003748 if message['disapproval']:
3749 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003750 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003751 elif message['approval']:
3752 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003753 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003754 elif message['sender'] == data['owner_email']:
3755 color = Fore.MAGENTA
3756 else:
3757 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003758 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003759 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003760 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003761 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003762 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003763 if options.json_file:
3764 with open(options.json_file, 'wb') as f:
3765 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003766 return 0
3767
3768
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003769@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003770def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003771 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003772 parser.add_option('-d', '--display', action='store_true',
3773 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003774 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003775 help='New description to set for this issue (- for stdin, '
3776 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003777 parser.add_option('-f', '--force', action='store_true',
3778 help='Delete any unpublished Gerrit edits for this issue '
3779 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003780
3781 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003782 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003783 options, args = parser.parse_args(args)
3784 _process_codereview_select_options(parser, options)
3785
3786 target_issue = None
3787 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003788 target_issue = ParseIssueNumberArgument(args[0])
3789 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003790 parser.print_help()
3791 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003792
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003793 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003794
martiniss6eda05f2016-06-30 10:18:35 -07003795 kwargs = {
3796 'auth_config': auth_config,
3797 'codereview': options.forced_codereview,
3798 }
3799 if target_issue:
3800 kwargs['issue'] = target_issue.issue
3801 if options.forced_codereview == 'rietveld':
3802 kwargs['rietveld_server'] = target_issue.hostname
3803
3804 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003805
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003806 if not cl.GetIssue():
3807 DieWithError('This branch has no associated changelist.')
3808 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003809
smut@google.com34fb6b12015-07-13 20:03:26 +00003810 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003811 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003812 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003813
3814 if options.new_description:
3815 text = options.new_description
3816 if text == '-':
3817 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003818 elif text == '+':
3819 base_branch = cl.GetCommonAncestorWithUpstream()
3820 change = cl.GetChange(base_branch, None, local_description=True)
3821 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003822
3823 description.set_description(text)
3824 else:
3825 description.prompt()
3826
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003827 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003828 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003829 return 0
3830
3831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832def CreateDescriptionFromLog(args):
3833 """Pulls out the commit log to use as a base for the CL description."""
3834 log_args = []
3835 if len(args) == 1 and not args[0].endswith('.'):
3836 log_args = [args[0] + '..']
3837 elif len(args) == 1 and args[0].endswith('...'):
3838 log_args = [args[0][:-1]]
3839 elif len(args) == 2:
3840 log_args = [args[0] + '..' + args[1]]
3841 else:
3842 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003843 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003844
3845
thestig@chromium.org44202a22014-03-11 19:22:18 +00003846def CMDlint(parser, args):
3847 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003848 parser.add_option('--filter', action='append', metavar='-x,+y',
3849 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003850 auth.add_auth_options(parser)
3851 options, args = parser.parse_args(args)
3852 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003853
3854 # Access to a protected member _XX of a client class
3855 # pylint: disable=W0212
3856 try:
3857 import cpplint
3858 import cpplint_chromium
3859 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003860 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003861 return 1
3862
3863 # Change the current working directory before calling lint so that it
3864 # shows the correct base.
3865 previous_cwd = os.getcwd()
3866 os.chdir(settings.GetRoot())
3867 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003868 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003869 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3870 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003871 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003872 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003873 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003874
3875 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003876 command = args + files
3877 if options.filter:
3878 command = ['--filter=' + ','.join(options.filter)] + command
3879 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003880
3881 white_regex = re.compile(settings.GetLintRegex())
3882 black_regex = re.compile(settings.GetLintIgnoreRegex())
3883 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3884 for filename in filenames:
3885 if white_regex.match(filename):
3886 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003887 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003888 else:
3889 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3890 extra_check_functions)
3891 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003892 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003893 finally:
3894 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003895 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003896 if cpplint._cpplint_state.error_count != 0:
3897 return 1
3898 return 0
3899
3900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003901def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003902 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003903 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003905 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003906 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003907 auth.add_auth_options(parser)
3908 options, args = parser.parse_args(args)
3909 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910
sbc@chromium.org71437c02015-04-09 19:29:40 +00003911 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913 return 1
3914
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003915 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916 if args:
3917 base_branch = args[0]
3918 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003919 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003920 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003921
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003922 cl.RunHook(
3923 committing=not options.upload,
3924 may_prompt=False,
3925 verbose=options.verbose,
3926 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003927 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928
3929
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003930def GenerateGerritChangeId(message):
3931 """Returns Ixxxxxx...xxx change id.
3932
3933 Works the same way as
3934 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3935 but can be called on demand on all platforms.
3936
3937 The basic idea is to generate git hash of a state of the tree, original commit
3938 message, author/committer info and timestamps.
3939 """
3940 lines = []
3941 tree_hash = RunGitSilent(['write-tree'])
3942 lines.append('tree %s' % tree_hash.strip())
3943 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3944 if code == 0:
3945 lines.append('parent %s' % parent.strip())
3946 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3947 lines.append('author %s' % author.strip())
3948 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3949 lines.append('committer %s' % committer.strip())
3950 lines.append('')
3951 # Note: Gerrit's commit-hook actually cleans message of some lines and
3952 # whitespace. This code is not doing this, but it clearly won't decrease
3953 # entropy.
3954 lines.append(message)
3955 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3956 stdin='\n'.join(lines))
3957 return 'I%s' % change_hash.strip()
3958
3959
wittman@chromium.org455dc922015-01-26 20:15:50 +00003960def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3961 """Computes the remote branch ref to use for the CL.
3962
3963 Args:
3964 remote (str): The git remote for the CL.
3965 remote_branch (str): The git remote branch for the CL.
3966 target_branch (str): The target branch specified by the user.
3967 pending_prefix (str): The pending prefix from the settings.
3968 """
3969 if not (remote and remote_branch):
3970 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003971
wittman@chromium.org455dc922015-01-26 20:15:50 +00003972 if target_branch:
3973 # Cannonicalize branch references to the equivalent local full symbolic
3974 # refs, which are then translated into the remote full symbolic refs
3975 # below.
3976 if '/' not in target_branch:
3977 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3978 else:
3979 prefix_replacements = (
3980 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3981 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3982 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3983 )
3984 match = None
3985 for regex, replacement in prefix_replacements:
3986 match = re.search(regex, target_branch)
3987 if match:
3988 remote_branch = target_branch.replace(match.group(0), replacement)
3989 break
3990 if not match:
3991 # This is a branch path but not one we recognize; use as-is.
3992 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003993 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3994 # Handle the refs that need to land in different refs.
3995 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003996
wittman@chromium.org455dc922015-01-26 20:15:50 +00003997 # Create the true path to the remote branch.
3998 # Does the following translation:
3999 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4000 # * refs/remotes/origin/master -> refs/heads/master
4001 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4002 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4003 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4004 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4005 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4006 'refs/heads/')
4007 elif remote_branch.startswith('refs/remotes/branch-heads'):
4008 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4009 # If a pending prefix exists then replace refs/ with it.
4010 if pending_prefix:
4011 remote_branch = remote_branch.replace('refs/', pending_prefix)
4012 return remote_branch
4013
4014
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004015def cleanup_list(l):
4016 """Fixes a list so that comma separated items are put as individual items.
4017
4018 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4019 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4020 """
4021 items = sum((i.split(',') for i in l), [])
4022 stripped_items = (i.strip() for i in items)
4023 return sorted(filter(None, stripped_items))
4024
4025
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004026@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004027def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004028 """Uploads the current changelist to codereview.
4029
4030 Can skip dependency patchset uploads for a branch by running:
4031 git config branch.branch_name.skip-deps-uploads True
4032 To unset run:
4033 git config --unset branch.branch_name.skip-deps-uploads
4034 Can also set the above globally by using the --global flag.
4035 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004036 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4037 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004038 parser.add_option('--bypass-watchlists', action='store_true',
4039 dest='bypass_watchlists',
4040 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004041 parser.add_option('-f', action='store_true', dest='force',
4042 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004043 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004044 parser.add_option('-b', '--bug',
4045 help='pre-populate the bug number(s) for this issue. '
4046 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004047 parser.add_option('--message-file', dest='message_file',
4048 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004049 parser.add_option('-t', dest='title',
4050 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004051 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004052 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004053 help='reviewer email addresses')
4054 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004055 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004056 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004057 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004058 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004059 parser.add_option('--emulate_svn_auto_props',
4060 '--emulate-svn-auto-props',
4061 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004062 dest="emulate_svn_auto_props",
4063 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004064 parser.add_option('-c', '--use-commit-queue', action='store_true',
4065 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004066 parser.add_option('--private', action='store_true',
4067 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004068 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004069 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004070 metavar='TARGET',
4071 help='Apply CL to remote ref TARGET. ' +
4072 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004073 parser.add_option('--squash', action='store_true',
4074 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004075 parser.add_option('--no-squash', action='store_true',
4076 help='Don\'t squash multiple commits into one ' +
4077 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004078 parser.add_option('--topic', default=None,
4079 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004080 parser.add_option('--email', default=None,
4081 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004082 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4083 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004084 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4085 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004086 help='Send the patchset to do a CQ dry run right after '
4087 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004088 parser.add_option('--dependencies', action='store_true',
4089 help='Uploads CLs of all the local branches that depend on '
4090 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004091
rmistry@google.com2dd99862015-06-22 12:22:18 +00004092 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004093 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004094 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004095 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004096 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004097 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004098 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004099
sbc@chromium.org71437c02015-04-09 19:29:40 +00004100 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004101 return 1
4102
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004103 options.reviewers = cleanup_list(options.reviewers)
4104 options.cc = cleanup_list(options.cc)
4105
tandriib80458a2016-06-23 12:20:07 -07004106 if options.message_file:
4107 if options.message:
4108 parser.error('only one of --message and --message-file allowed.')
4109 options.message = gclient_utils.FileRead(options.message_file)
4110 options.message_file = None
4111
tandrii4d0545a2016-07-06 03:56:49 -07004112 if options.cq_dry_run and options.use_commit_queue:
4113 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4114
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004115 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4116 settings.GetIsGerrit()
4117
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004118 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004119 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004120
4121
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004122def IsSubmoduleMergeCommit(ref):
4123 # When submodules are added to the repo, we expect there to be a single
4124 # non-git-svn merge commit at remote HEAD with a signature comment.
4125 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004126 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004127 return RunGit(cmd) != ''
4128
4129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004131 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004132
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004133 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4134 upstream and closes the issue automatically and atomically.
4135
4136 Otherwise (in case of Rietveld):
4137 Squashes branch into a single commit.
4138 Updates changelog with metadata (e.g. pointer to review).
4139 Pushes/dcommits the code upstream.
4140 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004141 """
4142 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4143 help='bypass upload presubmit hook')
4144 parser.add_option('-m', dest='message',
4145 help="override review description")
4146 parser.add_option('-f', action='store_true', dest='force',
4147 help="force yes to questions (don't prompt)")
4148 parser.add_option('-c', dest='contributor',
4149 help="external contributor for patch (appended to " +
4150 "description and used as author for git). Should be " +
4151 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004152 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004153 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004154 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004155 auth_config = auth.extract_auth_config_from_options(options)
4156
4157 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004158
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004159 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4160 if cl.IsGerrit():
4161 if options.message:
4162 # This could be implemented, but it requires sending a new patch to
4163 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4164 # Besides, Gerrit has the ability to change the commit message on submit
4165 # automatically, thus there is no need to support this option (so far?).
4166 parser.error('-m MESSAGE option is not supported for Gerrit.')
4167 if options.contributor:
4168 parser.error(
4169 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4170 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4171 'the contributor\'s "name <email>". If you can\'t upload such a '
4172 'commit for review, contact your repository admin and request'
4173 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004174 if not cl.GetIssue():
4175 DieWithError('You must upload the issue first to Gerrit.\n'
4176 ' If you would rather have `git cl land` upload '
4177 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004178 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4179 options.verbose)
4180
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004181 current = cl.GetBranch()
4182 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4183 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004184 print()
4185 print('Attempting to push branch %r into another local branch!' % current)
4186 print()
4187 print('Either reparent this branch on top of origin/master:')
4188 print(' git reparent-branch --root')
4189 print()
4190 print('OR run `git rebase-update` if you think the parent branch is ')
4191 print('already committed.')
4192 print()
4193 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004194 return 1
4195
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004196 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004197 # Default to merging against our best guess of the upstream branch.
4198 args = [cl.GetUpstreamBranch()]
4199
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004200 if options.contributor:
4201 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004202 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004203 return 1
4204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004206 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207
sbc@chromium.org71437c02015-04-09 19:29:40 +00004208 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209 return 1
4210
4211 # This rev-list syntax means "show all commits not in my branch that
4212 # are in base_branch".
4213 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4214 base_branch]).splitlines()
4215 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print('Base branch "%s" has %d commits '
4217 'not in this branch.' % (base_branch, len(upstream_commits)))
4218 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219 return 1
4220
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004221 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004222 svn_head = None
4223 if cmd == 'dcommit' or base_has_submodules:
4224 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4225 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004227 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004228 # If the base_head is a submodule merge commit, the first parent of the
4229 # base_head should be a git-svn commit, which is what we're interested in.
4230 base_svn_head = base_branch
4231 if base_has_submodules:
4232 base_svn_head += '^1'
4233
4234 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004236 print('This branch has %d additional commits not upstreamed yet.'
4237 % len(extra_commits.splitlines()))
4238 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4239 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240 return 1
4241
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004242 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004243 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004244 author = None
4245 if options.contributor:
4246 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004247 hook_results = cl.RunHook(
4248 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004249 may_prompt=not options.force,
4250 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004251 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004252 if not hook_results.should_continue():
4253 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004254
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004255 # Check the tree status if the tree status URL is set.
4256 status = GetTreeStatus()
4257 if 'closed' == status:
4258 print('The tree is closed. Please wait for it to reopen. Use '
4259 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4260 return 1
4261 elif 'unknown' == status:
4262 print('Unable to determine tree status. Please verify manually and '
4263 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4264 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004266 change_desc = ChangeDescription(options.message)
4267 if not change_desc.description and cl.GetIssue():
4268 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004270 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004271 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004272 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004273 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004274 print('No description set.')
4275 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004276 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004277
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004278 # Keep a separate copy for the commit message, because the commit message
4279 # contains the link to the Rietveld issue, while the Rietveld message contains
4280 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004281 # Keep a separate copy for the commit message.
4282 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004283 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004284
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004285 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004286 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004287 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004288 # after it. Add a period on a new line to circumvent this. Also add a space
4289 # before the period to make sure that Gitiles continues to correctly resolve
4290 # the URL.
4291 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004292 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004293 commit_desc.append_footer('Patch from %s.' % options.contributor)
4294
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004295 print('Description:')
4296 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004297
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004298 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004300 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004302 # We want to squash all this branch's commits into one commit with the proper
4303 # description. We do this by doing a "reset --soft" to the base branch (which
4304 # keeps the working copy the same), then dcommitting that. If origin/master
4305 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4306 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004307 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004308 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4309 # Delete the branches if they exist.
4310 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4311 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4312 result = RunGitWithCode(showref_cmd)
4313 if result[0] == 0:
4314 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315
4316 # We might be in a directory that's present in this branch but not in the
4317 # trunk. Move up to the top of the tree so that git commands that expect a
4318 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004319 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 if rel_base_path:
4321 os.chdir(rel_base_path)
4322
4323 # Stuff our change into the merge branch.
4324 # We wrap in a try...finally block so if anything goes wrong,
4325 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004326 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004327 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004328 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004329 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004331 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004332 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004333 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004334 RunGit(
4335 [
4336 'commit', '--author', options.contributor,
4337 '-m', commit_desc.description,
4338 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004340 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004341 if base_has_submodules:
4342 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4343 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4344 RunGit(['checkout', CHERRY_PICK_BRANCH])
4345 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004346 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004347 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004348 mirror = settings.GetGitMirror(remote)
4349 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004350 pending_prefix = settings.GetPendingRefPrefix()
4351 if not pending_prefix or branch.startswith(pending_prefix):
4352 # If not using refs/pending/heads/* at all, or target ref is already set
4353 # to pending, then push to the target ref directly.
4354 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004355 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004356 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004357 else:
4358 # Cherry-pick the change on top of pending ref and then push it.
4359 assert branch.startswith('refs/'), branch
4360 assert pending_prefix[-1] == '/', pending_prefix
4361 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004362 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004363 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004364 if retcode == 0:
4365 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366 else:
4367 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004368 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004369 'svn', 'dcommit',
4370 '-C%s' % options.similarity,
4371 '--no-rebase', '--rmdir',
4372 ]
4373 if settings.GetForceHttpsCommitUrl():
4374 # Allow forcing https commit URLs for some projects that don't allow
4375 # committing to http URLs (like Google Code).
4376 remote_url = cl.GetGitSvnRemoteUrl()
4377 if urlparse.urlparse(remote_url).scheme == 'http':
4378 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004379 cmd_args.append('--commit-url=%s' % remote_url)
4380 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004381 if 'Committed r' in output:
4382 revision = re.match(
4383 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4384 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385 finally:
4386 # And then swap back to the original branch and clean up.
4387 RunGit(['checkout', '-q', cl.GetBranch()])
4388 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004389 if base_has_submodules:
4390 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004392 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004394 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004395
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004396 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004397 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004398 try:
4399 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4400 # We set pushed_to_pending to False, since it made it all the way to the
4401 # real ref.
4402 pushed_to_pending = False
4403 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004404 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004405
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004407 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004408 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004409 if not to_pending:
4410 if viewvc_url and revision:
4411 change_desc.append_footer(
4412 'Committed: %s%s' % (viewvc_url, revision))
4413 elif revision:
4414 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004415 print('Closing issue '
4416 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004417 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004419 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004420 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004421 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004422 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004423 if options.bypass_hooks:
4424 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4425 else:
4426 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004427 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004428
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004429 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004430 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004431 print('The commit is in the pending queue (%s).' % pending_ref)
4432 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4433 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004434
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004435 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4436 if os.path.isfile(hook):
4437 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004438
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004439 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004440
4441
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004442def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004443 print()
4444 print('Waiting for commit to be landed on %s...' % real_ref)
4445 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004446 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4447 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004448 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004449
4450 loop = 0
4451 while True:
4452 sys.stdout.write('fetching (%d)... \r' % loop)
4453 sys.stdout.flush()
4454 loop += 1
4455
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004456 if mirror:
4457 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004458 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4459 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4460 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4461 for commit in commits.splitlines():
4462 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004464 return commit
4465
4466 current_rev = to_rev
4467
4468
tandriibf429402016-09-14 07:09:12 -07004469def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004470 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4471
4472 Returns:
4473 (retcode of last operation, output log of last operation).
4474 """
4475 assert pending_ref.startswith('refs/'), pending_ref
4476 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4477 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4478 code = 0
4479 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004480 max_attempts = 3
4481 attempts_left = max_attempts
4482 while attempts_left:
4483 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004484 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004485 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004486
4487 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004488 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004489 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004490 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004491 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004492 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004493 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004494 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004495 continue
4496
4497 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004498 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004499 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004500 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004501 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004502 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4503 'the following files have merge conflicts:' % pending_ref)
4504 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4505 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004506 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004507 return code, out
4508
4509 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004510 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004511 code, out = RunGitWithCode(
4512 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4513 if code == 0:
4514 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004515 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004516 return code, out
4517
vapiera7fbd5a2016-06-16 09:17:49 -07004518 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004519 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004520 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004521 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004522 print('Fatal push error. Make sure your .netrc credentials and git '
4523 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004524 return code, out
4525
vapiera7fbd5a2016-06-16 09:17:49 -07004526 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004527 return code, out
4528
4529
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004530def IsFatalPushFailure(push_stdout):
4531 """True if retrying push won't help."""
4532 return '(prohibited by Gerrit)' in push_stdout
4533
4534
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004535@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004537 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004538 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004539 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004540 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004541 message = """This repository appears to be a git-svn mirror, but we
4542don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004543 else:
4544 message = """This doesn't appear to be an SVN repository.
4545If your project has a true, writeable git repository, you probably want to run
4546'git cl land' instead.
4547If your project has a git mirror of an upstream SVN master, you probably need
4548to run 'git svn init'.
4549
4550Using the wrong command might cause your commit to appear to succeed, and the
4551review to be closed, without actually landing upstream. If you choose to
4552proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004553 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004554 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004555 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4556 'Please let us know of this project you are committing to:'
4557 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004558 return SendUpstream(parser, args, 'dcommit')
4559
4560
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004561@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004562def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004563 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004564 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004565 print('This appears to be an SVN repository.')
4566 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004567 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004568 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004569 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004570
4571
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004572@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004573def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004574 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004575 parser.add_option('-b', dest='newbranch',
4576 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004577 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004578 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004579 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4580 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004581 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004582 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004583 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004584 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004585 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004586 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004587
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004588
4589 group = optparse.OptionGroup(
4590 parser,
4591 'Options for continuing work on the current issue uploaded from a '
4592 'different clone (e.g. different machine). Must be used independently '
4593 'from the other options. No issue number should be specified, and the '
4594 'branch must have an issue number associated with it')
4595 group.add_option('--reapply', action='store_true', dest='reapply',
4596 help='Reset the branch and reapply the issue.\n'
4597 'CAUTION: This will undo any local changes in this '
4598 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004599
4600 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004601 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004602 parser.add_option_group(group)
4603
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004604 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004605 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004607 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004608 auth_config = auth.extract_auth_config_from_options(options)
4609
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004610
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004611 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004612 if options.newbranch:
4613 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004614 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004615 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004616
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004617 cl = Changelist(auth_config=auth_config,
4618 codereview=options.forced_codereview)
4619 if not cl.GetIssue():
4620 parser.error('current branch must have an associated issue')
4621
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004622 upstream = cl.GetUpstreamBranch()
4623 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004624 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004625
4626 RunGit(['reset', '--hard', upstream])
4627 if options.pull:
4628 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004629
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004630 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4631 options.directory)
4632
4633 if len(args) != 1 or not args[0]:
4634 parser.error('Must specify issue number or url')
4635
4636 # We don't want uncommitted changes mixed up with the patch.
4637 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004638 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004640 if options.newbranch:
4641 if options.force:
4642 RunGit(['branch', '-D', options.newbranch],
4643 stderr=subprocess2.PIPE, error_ok=True)
4644 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004645 elif not GetCurrentBranch():
4646 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004647
4648 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4649
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004650 if cl.IsGerrit():
4651 if options.reject:
4652 parser.error('--reject is not supported with Gerrit codereview.')
4653 if options.nocommit:
4654 parser.error('--nocommit is not supported with Gerrit codereview.')
4655 if options.directory:
4656 parser.error('--directory is not supported with Gerrit codereview.')
4657
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004658 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004659 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660
4661
4662def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004663 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004664 # Provide a wrapper for git svn rebase to help avoid accidental
4665 # git svn dcommit.
4666 # It's the only command that doesn't use parser at all since we just defer
4667 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004668
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004669 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004670
4671
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004672def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004673 """Fetches the tree status and returns either 'open', 'closed',
4674 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004675 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004676 if url:
4677 status = urllib2.urlopen(url).read().lower()
4678 if status.find('closed') != -1 or status == '0':
4679 return 'closed'
4680 elif status.find('open') != -1 or status == '1':
4681 return 'open'
4682 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004683 return 'unset'
4684
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004685
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004686def GetTreeStatusReason():
4687 """Fetches the tree status from a json url and returns the message
4688 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004689 url = settings.GetTreeStatusUrl()
4690 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691 connection = urllib2.urlopen(json_url)
4692 status = json.loads(connection.read())
4693 connection.close()
4694 return status['message']
4695
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004696
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004697def GetBuilderMaster(bot_list):
4698 """For a given builder, fetch the master from AE if available."""
4699 map_url = 'https://builders-map.appspot.com/'
4700 try:
4701 master_map = json.load(urllib2.urlopen(map_url))
4702 except urllib2.URLError as e:
4703 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4704 (map_url, e))
4705 except ValueError as e:
4706 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4707 if not master_map:
4708 return None, 'Failed to build master map.'
4709
4710 result_master = ''
4711 for bot in bot_list:
4712 builder = bot.split(':', 1)[0]
4713 master_list = master_map.get(builder, [])
4714 if not master_list:
4715 return None, ('No matching master for builder %s.' % builder)
4716 elif len(master_list) > 1:
4717 return None, ('The builder name %s exists in multiple masters %s.' %
4718 (builder, master_list))
4719 else:
4720 cur_master = master_list[0]
4721 if not result_master:
4722 result_master = cur_master
4723 elif result_master != cur_master:
4724 return None, 'The builders do not belong to the same master.'
4725 return result_master, None
4726
4727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004728def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004729 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004730 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004731 status = GetTreeStatus()
4732 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004733 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734 return 2
4735
vapiera7fbd5a2016-06-16 09:17:49 -07004736 print('The tree is %s' % status)
4737 print()
4738 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004739 if status != 'open':
4740 return 1
4741 return 0
4742
4743
maruel@chromium.org15192402012-09-06 12:38:29 +00004744def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004745 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4746 """
tandrii1838bad2016-10-06 00:10:52 -07004747 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004748 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004749 '-b', '--bot', action='append',
4750 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4751 'times to specify multiple builders. ex: '
4752 '"-b win_rel -b win_layout". See '
4753 'the try server waterfall for the builders name and the tests '
4754 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004755 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004756 '-B', '--bucket', default='',
4757 help=('Buildbucket bucket to send the try requests.'))
4758 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004759 '-m', '--master', default='',
4760 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004761 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004762 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004763 help='Revision to use for the try job; default: the revision will '
4764 'be determined by the try recipe that builder runs, which usually '
4765 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004766 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004767 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004768 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004769 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004770 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004771 '--project',
4772 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004773 'in recipe to determine to which repository or directory to '
4774 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004775 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004776 '-p', '--property', dest='properties', action='append', default=[],
4777 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004778 'key2=value2 etc. The value will be treated as '
4779 'json if decodable, or as string otherwise. '
4780 'NOTE: using this may make your try job not usable for CQ, '
4781 'which will then schedule another try job with default properties')
4782 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004783 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004784 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004785 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004786 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4787 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004788 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004789 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004790 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004791 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004792
machenbach@chromium.org45453142015-09-15 08:45:22 +00004793 # Make sure that all properties are prop=value pairs.
4794 bad_params = [x for x in options.properties if '=' not in x]
4795 if bad_params:
4796 parser.error('Got properties with missing "=": %s' % bad_params)
4797
maruel@chromium.org15192402012-09-06 12:38:29 +00004798 if args:
4799 parser.error('Unknown arguments: %s' % args)
4800
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004801 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004802 if not cl.GetIssue():
4803 parser.error('Need to upload first')
4804
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004805 if cl.IsGerrit():
4806 parser.error(
4807 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4808 'If your project has Commit Queue, dry run is a workaround:\n'
4809 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004810
tandriie113dfd2016-10-11 10:20:12 -07004811 error_message = cl.CannotTriggerTryJobReason()
4812 if error_message:
4813 parser.error('Can\'t trigger try jobs: %s')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004814
maruel@chromium.org15192402012-09-06 12:38:29 +00004815 if not options.name:
4816 options.name = cl.GetBranch()
4817
borenet6c0efe62016-10-19 08:13:29 -07004818 if options.bucket and options.master:
4819 parser.error('Only one of --bucket and --master may be used.')
4820
4821 if options.bot and not options.master and not options.bucket:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004822 options.master, err_msg = GetBuilderMaster(options.bot)
4823 if err_msg:
4824 parser.error('Tryserver master cannot be found because: %s\n'
4825 'Please manually specify the tryserver master'
4826 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004827
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004828 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004829 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004830 if not options.bot:
4831 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004832
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004833 # Get try masters from PRESUBMIT.py files.
4834 masters = presubmit_support.DoGetTryMasters(
4835 change,
4836 change.LocalPaths(),
4837 settings.GetRoot(),
4838 None,
4839 None,
4840 options.verbose,
4841 sys.stdout)
4842 if masters:
4843 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004844
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004845 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4846 options.bot = presubmit_support.DoGetTrySlaves(
4847 change,
4848 change.LocalPaths(),
4849 settings.GetRoot(),
4850 None,
4851 None,
4852 options.verbose,
4853 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004854
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004855 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004856 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004857
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004858 builders_and_tests = {}
4859 # TODO(machenbach): The old style command-line options don't support
4860 # multiple try masters yet.
4861 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4862 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4863
4864 for bot in old_style:
4865 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004866 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004867 elif ',' in bot:
4868 parser.error('Specify one bot per --bot flag')
4869 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004870 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004871
4872 for bot, tests in new_style:
4873 builders_and_tests.setdefault(bot, []).extend(tests)
4874
4875 # Return a master map with one master to be backwards compatible. The
4876 # master name defaults to an empty string, which will cause the master
4877 # not to be set on rietveld (deprecated).
borenet6c0efe62016-10-19 08:13:29 -07004878 bucket = ''
4879 if options.master:
4880 # Add the "master." prefix to the master name to obtain the bucket name.
4881 bucket = _prefix_master(options.master)
4882 return {bucket: builders_and_tests}
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004883
borenet6c0efe62016-10-19 08:13:29 -07004884 if options.bucket:
4885 buckets = {options.bucket: {b: [] for b in options.bot}}
4886 else:
4887 buckets = GetMasterMap()
4888 if not buckets:
4889 # Default to triggering Dry Run (see http://crbug.com/625697).
4890 if options.verbose:
4891 print('git cl try with no bots now defaults to CQ Dry Run.')
4892 try:
4893 cl.SetCQState(_CQState.DRY_RUN)
4894 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4895 return 0
4896 except KeyboardInterrupt:
4897 raise
4898 except:
4899 print('WARNING: failed to trigger CQ Dry Run.\n'
4900 'Either:\n'
4901 ' * your project has no CQ\n'
4902 ' * you don\'t have permission to trigger Dry Run\n'
4903 ' * bug in this code (see stack trace below).\n'
4904 'Consider specifying which bots to trigger manually '
4905 'or asking your project owners for permissions '
4906 'or contacting Chrome Infrastructure team at '
4907 'https://www.chromium.org/infra\n\n')
4908 # Still raise exception so that stack trace is printed.
4909 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004910
borenet6c0efe62016-10-19 08:13:29 -07004911 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004912 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004913 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004914 'of bot requires an initial job from a parent (usually a builder). '
4915 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004916 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004917 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004918
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004919 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004920 if patchset != cl.GetPatchset():
4921 print('Warning: Codereview server has newer patchsets (%s) than most '
4922 'recent upload from local checkout (%s). Did a previous upload '
4923 'fail?\n'
4924 'By default, git cl try uses the latest patchset from '
4925 'codereview, continuing to use patchset %s.\n' %
4926 (patchset, cl.GetPatchset(), patchset))
tandrii568043b2016-10-11 07:49:18 -07004927 try:
borenet6c0efe62016-10-19 08:13:29 -07004928 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4929 patchset)
tandrii568043b2016-10-11 07:49:18 -07004930 except BuildbucketResponseException as ex:
4931 print('ERROR: %s' % ex)
4932 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004933 return 0
4934
4935
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004936def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004937 """Prints info about try jobs associated with current CL."""
4938 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004939 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004940 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004941 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004942 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004943 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004944 '--color', action='store_true', default=setup_color.IS_TTY,
4945 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004946 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004947 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4948 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004949 group.add_option(
4950 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004951 parser.add_option_group(group)
4952 auth.add_auth_options(parser)
4953 options, args = parser.parse_args(args)
4954 if args:
4955 parser.error('Unrecognized args: %s' % ' '.join(args))
4956
4957 auth_config = auth.extract_auth_config_from_options(options)
4958 cl = Changelist(auth_config=auth_config)
4959 if not cl.GetIssue():
4960 parser.error('Need to upload first')
4961
tandrii221ab252016-10-06 08:12:04 -07004962 patchset = options.patchset
4963 if not patchset:
4964 patchset = cl.GetMostRecentPatchset()
4965 if not patchset:
4966 parser.error('Codereview doesn\'t know about issue %s. '
4967 'No access to issue or wrong issue number?\n'
4968 'Either upload first, or pass --patchset explicitely' %
4969 cl.GetIssue())
4970
4971 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004972 print('Warning: Codereview server has newer patchsets (%s) than most '
4973 'recent upload from local checkout (%s). Did a previous upload '
4974 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004975 'By default, git cl try-results uses the latest patchset from '
4976 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004977 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004978 try:
tandrii221ab252016-10-06 08:12:04 -07004979 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004980 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004981 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004982 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004983 if options.json:
4984 write_try_results_json(options.json, jobs)
4985 else:
4986 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004987 return 0
4988
4989
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004990@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004991def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004992 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004993 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004994 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004995 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004996
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004997 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004998 if args:
4999 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005000 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005001 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005002 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005003 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005004
5005 # Clear configured merge-base, if there is one.
5006 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005007 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005008 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005009 return 0
5010
5011
thestig@chromium.org00858c82013-12-02 23:08:03 +00005012def CMDweb(parser, args):
5013 """Opens the current CL in the web browser."""
5014 _, args = parser.parse_args(args)
5015 if args:
5016 parser.error('Unrecognized args: %s' % ' '.join(args))
5017
5018 issue_url = Changelist().GetIssueURL()
5019 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005020 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005021 return 1
5022
5023 webbrowser.open(issue_url)
5024 return 0
5025
5026
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005027def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005028 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005029 parser.add_option('-d', '--dry-run', action='store_true',
5030 help='trigger in dry run mode')
5031 parser.add_option('-c', '--clear', action='store_true',
5032 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005033 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005034 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005035 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005036 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005037 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005038 if args:
5039 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005040 if options.dry_run and options.clear:
5041 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5042
iannuccie53c9352016-08-17 14:40:40 -07005043 cl = Changelist(auth_config=auth_config, issue=options.issue,
5044 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005045 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005046 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005047 elif options.dry_run:
5048 state = _CQState.DRY_RUN
5049 else:
5050 state = _CQState.COMMIT
5051 if not cl.GetIssue():
5052 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005053 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005054 return 0
5055
5056
groby@chromium.org411034a2013-02-26 15:12:01 +00005057def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005058 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005059 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005060 auth.add_auth_options(parser)
5061 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005062 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005063 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005064 if args:
5065 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005066 cl = Changelist(auth_config=auth_config, issue=options.issue,
5067 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005068 # Ensure there actually is an issue to close.
5069 cl.GetDescription()
5070 cl.CloseIssue()
5071 return 0
5072
5073
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005074def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005075 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005076 parser.add_option(
5077 '--stat',
5078 action='store_true',
5079 dest='stat',
5080 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005081 auth.add_auth_options(parser)
5082 options, args = parser.parse_args(args)
5083 auth_config = auth.extract_auth_config_from_options(options)
5084 if args:
5085 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005086
5087 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005088 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005089 # Staged changes would be committed along with the patch from last
5090 # upload, hence counted toward the "last upload" side in the final
5091 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005092 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005093 return 1
5094
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005095 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005096 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005097 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005098 if not issue:
5099 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005100 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005101 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005102
5103 # Create a new branch based on the merge-base
5104 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005105 # Clear cached branch in cl object, to avoid overwriting original CL branch
5106 # properties.
5107 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005108 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005109 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005110 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005111 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005112 return rtn
5113
wychen@chromium.org06928532015-02-03 02:11:29 +00005114 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005115 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005116 cmd = ['git', 'diff']
5117 if options.stat:
5118 cmd.append('--stat')
5119 cmd.extend([TMP_BRANCH, branch, '--'])
5120 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005121 finally:
5122 RunGit(['checkout', '-q', branch])
5123 RunGit(['branch', '-D', TMP_BRANCH])
5124
5125 return 0
5126
5127
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005128def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005129 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005130 parser.add_option(
5131 '--no-color',
5132 action='store_true',
5133 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005134 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005135 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005136 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005137
5138 author = RunGit(['config', 'user.email']).strip() or None
5139
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005140 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005141
5142 if args:
5143 if len(args) > 1:
5144 parser.error('Unknown args')
5145 base_branch = args[0]
5146 else:
5147 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005148 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005149
5150 change = cl.GetChange(base_branch, None)
5151 return owners_finder.OwnersFinder(
5152 [f.LocalPath() for f in
5153 cl.GetChange(base_branch, None).AffectedFiles()],
5154 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005155 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005156 disable_color=options.no_color).run()
5157
5158
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005159def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005160 """Generates a diff command."""
5161 # Generate diff for the current branch's changes.
5162 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5163 upstream_commit, '--' ]
5164
5165 if args:
5166 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005167 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005168 diff_cmd.append(arg)
5169 else:
5170 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005171
5172 return diff_cmd
5173
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005174def MatchingFileType(file_name, extensions):
5175 """Returns true if the file name ends with one of the given extensions."""
5176 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005177
enne@chromium.org555cfe42014-01-29 18:21:39 +00005178@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005179def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005180 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005181 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005182 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005183 parser.add_option('--full', action='store_true',
5184 help='Reformat the full content of all touched files')
5185 parser.add_option('--dry-run', action='store_true',
5186 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005187 parser.add_option('--python', action='store_true',
5188 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005189 parser.add_option('--diff', action='store_true',
5190 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005191 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005192
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005193 # git diff generates paths against the root of the repository. Change
5194 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005195 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005196 if rel_base_path:
5197 os.chdir(rel_base_path)
5198
digit@chromium.org29e47272013-05-17 17:01:46 +00005199 # Grab the merge-base commit, i.e. the upstream commit of the current
5200 # branch when it was created or the last time it was rebased. This is
5201 # to cover the case where the user may have called "git fetch origin",
5202 # moving the origin branch to a newer commit, but hasn't rebased yet.
5203 upstream_commit = None
5204 cl = Changelist()
5205 upstream_branch = cl.GetUpstreamBranch()
5206 if upstream_branch:
5207 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5208 upstream_commit = upstream_commit.strip()
5209
5210 if not upstream_commit:
5211 DieWithError('Could not find base commit for this branch. '
5212 'Are you in detached state?')
5213
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005214 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5215 diff_output = RunGit(changed_files_cmd)
5216 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005217 # Filter out files deleted by this CL
5218 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005219
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005220 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5221 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5222 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005223 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005224
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005225 top_dir = os.path.normpath(
5226 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5227
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005228 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5229 # formatted. This is used to block during the presubmit.
5230 return_value = 0
5231
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005232 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005233 # Locate the clang-format binary in the checkout
5234 try:
5235 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005236 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005237 DieWithError(e)
5238
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005239 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005240 cmd = [clang_format_tool]
5241 if not opts.dry_run and not opts.diff:
5242 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005243 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005244 if opts.diff:
5245 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005246 else:
5247 env = os.environ.copy()
5248 env['PATH'] = str(os.path.dirname(clang_format_tool))
5249 try:
5250 script = clang_format.FindClangFormatScriptInChromiumTree(
5251 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005252 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005253 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005254
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005255 cmd = [sys.executable, script, '-p0']
5256 if not opts.dry_run and not opts.diff:
5257 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005258
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005259 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5260 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005261
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005262 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5263 if opts.diff:
5264 sys.stdout.write(stdout)
5265 if opts.dry_run and len(stdout) > 0:
5266 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005267
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005268 # Similar code to above, but using yapf on .py files rather than clang-format
5269 # on C/C++ files
5270 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005271 yapf_tool = gclient_utils.FindExecutable('yapf')
5272 if yapf_tool is None:
5273 DieWithError('yapf not found in PATH')
5274
5275 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005276 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005277 cmd = [yapf_tool]
5278 if not opts.dry_run and not opts.diff:
5279 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005280 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005281 if opts.diff:
5282 sys.stdout.write(stdout)
5283 else:
5284 # TODO(sbc): yapf --lines mode still has some issues.
5285 # https://github.com/google/yapf/issues/154
5286 DieWithError('--python currently only works with --full')
5287
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005288 # Dart's formatter does not have the nice property of only operating on
5289 # modified chunks, so hard code full.
5290 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005291 try:
5292 command = [dart_format.FindDartFmtToolInChromiumTree()]
5293 if not opts.dry_run and not opts.diff:
5294 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005295 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005296
ppi@chromium.org6593d932016-03-03 15:41:15 +00005297 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005298 if opts.dry_run and stdout:
5299 return_value = 2
5300 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005301 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5302 'found in this checkout. Files in other languages are still '
5303 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005304
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005305 # Format GN build files. Always run on full build files for canonical form.
5306 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005307 cmd = ['gn', 'format' ]
5308 if opts.dry_run or opts.diff:
5309 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005310 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005311 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5312 shell=sys.platform == 'win32',
5313 cwd=top_dir)
5314 if opts.dry_run and gn_ret == 2:
5315 return_value = 2 # Not formatted.
5316 elif opts.diff and gn_ret == 2:
5317 # TODO this should compute and print the actual diff.
5318 print("This change has GN build file diff for " + gn_diff_file)
5319 elif gn_ret != 0:
5320 # For non-dry run cases (and non-2 return values for dry-run), a
5321 # nonzero error code indicates a failure, probably because the file
5322 # doesn't parse.
5323 DieWithError("gn format failed on " + gn_diff_file +
5324 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005325
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005326 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005327
5328
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005329@subcommand.usage('<codereview url or issue id>')
5330def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005331 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005332 _, args = parser.parse_args(args)
5333
5334 if len(args) != 1:
5335 parser.print_help()
5336 return 1
5337
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005338 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005339 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005340 parser.print_help()
5341 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005342 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005343
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005344 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005345 output = RunGit(['config', '--local', '--get-regexp',
5346 r'branch\..*\.%s' % issueprefix],
5347 error_ok=True)
5348 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005349 if issue == target_issue:
5350 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005351
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005352 branches = []
5353 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005354 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005355 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005356 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005357 return 1
5358 if len(branches) == 1:
5359 RunGit(['checkout', branches[0]])
5360 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005362 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005363 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005364 which = raw_input('Choose by index: ')
5365 try:
5366 RunGit(['checkout', branches[int(which)]])
5367 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005368 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005369 return 1
5370
5371 return 0
5372
5373
maruel@chromium.org29404b52014-09-08 22:58:00 +00005374def CMDlol(parser, args):
5375 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005376 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005377 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5378 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5379 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005380 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005381 return 0
5382
5383
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005384class OptionParser(optparse.OptionParser):
5385 """Creates the option parse and add --verbose support."""
5386 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005387 optparse.OptionParser.__init__(
5388 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005389 self.add_option(
5390 '-v', '--verbose', action='count', default=0,
5391 help='Use 2 times for more debugging info')
5392
5393 def parse_args(self, args=None, values=None):
5394 options, args = optparse.OptionParser.parse_args(self, args, values)
5395 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5396 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5397 return options, args
5398
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005399
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005400def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005401 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005402 print('\nYour python version %s is unsupported, please upgrade.\n' %
5403 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005404 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005405
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005406 # Reload settings.
5407 global settings
5408 settings = Settings()
5409
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005410 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005411 dispatcher = subcommand.CommandDispatcher(__name__)
5412 try:
5413 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005414 except auth.AuthenticationError as e:
5415 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005416 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005417 if e.code != 500:
5418 raise
5419 DieWithError(
5420 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5421 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005422 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005423
5424
5425if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005426 # These affect sys.stdout so do it outside of main() to simplify mocks in
5427 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005428 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005429 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005430 try:
5431 sys.exit(main(sys.argv[1:]))
5432 except KeyboardInterrupt:
5433 sys.stderr.write('interrupted\n')
5434 sys.exit(1)