blob: 79fceab2431323208297b9807606d383da50ae00 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
skobes6468b902016-10-24 08:45:10 -070043import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
88
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070090 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091 sys.exit(1)
92
93
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000094def GetNoGitPagerEnv():
95 env = os.environ.copy()
96 # 'cat' is a magical git string that disables pagers on all platforms.
97 env['GIT_PAGER'] = 'cat'
98 return env
99
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000100
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000104 except subprocess2.CalledProcessError as e:
105 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 'Command "%s" failed.\n%s' % (
109 ' '.join(args), error_message or e.stdout or ''))
110 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
113def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000115 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000119 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 try:
tandrii5d48c322016-08-18 16:19:37 -0700125 (out, _), code = subprocess2.communicate(['git'] + args,
126 env=GetNoGitPagerEnv(),
127 stdout=subprocess2.PIPE,
128 stderr=stderr)
129 return code, out
130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
132 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
tandrii2a16b952016-10-19 07:09:44 -0700154def time_sleep(seconds):
155 # Use this so that it can be mocked in tests without interfering with python
156 # system machinery.
157 import time # Local import to discourage others from importing time globally.
158 return time.sleep(seconds)
159
160
maruel@chromium.org90541732011-04-01 17:54:18 +0000161def ask_for_data(prompt):
162 try:
163 return raw_input(prompt)
164 except KeyboardInterrupt:
165 # Hide the exception.
166 sys.exit(1)
167
168
tandrii5d48c322016-08-18 16:19:37 -0700169def _git_branch_config_key(branch, key):
170 """Helper method to return Git config key for a branch."""
171 assert branch, 'branch name is required to set git config for it'
172 return 'branch.%s.%s' % (branch, key)
173
174
175def _git_get_branch_config_value(key, default=None, value_type=str,
176 branch=False):
177 """Returns git config value of given or current branch if any.
178
179 Returns default in all other cases.
180 """
181 assert value_type in (int, str, bool)
182 if branch is False: # Distinguishing default arg value from None.
183 branch = GetCurrentBranch()
184
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000185 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700186 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187
tandrii5d48c322016-08-18 16:19:37 -0700188 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700189 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700190 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700191 # git config also has --int, but apparently git config suffers from integer
192 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700193 args.append(_git_branch_config_key(branch, key))
194 code, out = RunGitWithCode(args)
195 if code == 0:
196 value = out.strip()
197 if value_type == int:
198 return int(value)
199 if value_type == bool:
200 return bool(value.lower() == 'true')
201 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 return default
203
204
tandrii5d48c322016-08-18 16:19:37 -0700205def _git_set_branch_config_value(key, value, branch=None, **kwargs):
206 """Sets the value or unsets if it's None of a git branch config.
207
208 Valid, though not necessarily existing, branch must be provided,
209 otherwise currently checked out branch is used.
210 """
211 if not branch:
212 branch = GetCurrentBranch()
213 assert branch, 'a branch name OR currently checked out branch is required'
214 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700215 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700216 if value is None:
217 args.append('--unset')
218 elif isinstance(value, bool):
219 args.append('--bool')
220 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700221 else:
tandrii33a46ff2016-08-23 05:53:40 -0700222 # git config also has --int, but apparently git config suffers from integer
223 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700224 value = str(value)
225 args.append(_git_branch_config_key(branch, key))
226 if value is not None:
227 args.append(value)
228 RunGit(args, **kwargs)
229
230
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000231def add_git_similarity(parser):
232 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700233 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000234 help='Sets the percentage that a pair of files need to match in order to'
235 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000236 parser.add_option(
237 '--find-copies', action='store_true',
238 help='Allows git to look for copies.')
239 parser.add_option(
240 '--no-find-copies', action='store_false', dest='find_copies',
241 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000242
243 old_parser_args = parser.parse_args
244 def Parse(args):
245 options, args = old_parser_args(args)
246
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000247 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.similarity = _git_get_branch_config_value(
249 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 print('Note: Saving similarity of %d%% in git config.'
252 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700253 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 options.similarity = max(0, min(options.similarity, 100))
256
257 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700258 options.find_copies = _git_get_branch_config_value(
259 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000260 else:
tandrii5d48c322016-08-18 16:19:37 -0700261 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 print('Using %d%% similarity for rename/copy detection. '
264 'Override with --similarity.' % options.similarity)
265
266 return options, args
267 parser.parse_args = Parse
268
269
machenbach@chromium.org45453142015-09-15 08:45:22 +0000270def _get_properties_from_options(options):
271 properties = dict(x.split('=', 1) for x in options.properties)
272 for key, val in properties.iteritems():
273 try:
274 properties[key] = json.loads(val)
275 except ValueError:
276 pass # If a value couldn't be evaluated, treat it as a string.
277 return properties
278
279
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280def _prefix_master(master):
281 """Convert user-specified master name to full master name.
282
283 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
284 name, while the developers always use shortened master name
285 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
286 function does the conversion for buildbucket migration.
287 """
borenet6c0efe62016-10-19 08:13:29 -0700288 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 return master
borenet6c0efe62016-10-19 08:13:29 -0700290 return '%s%s' % (MASTER_PREFIX, master)
291
292
293def _unprefix_master(bucket):
294 """Convert bucket name to shortened master name.
295
296 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
297 name, while the developers always use shortened master name
298 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
299 function does the conversion for buildbucket migration.
300 """
301 if bucket.startswith(MASTER_PREFIX):
302 return bucket[len(MASTER_PREFIX):]
303 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306def _buildbucket_retry(operation_name, http, *args, **kwargs):
307 """Retries requests to buildbucket service and returns parsed json content."""
308 try_count = 0
309 while True:
310 response, content = http.request(*args, **kwargs)
311 try:
312 content_json = json.loads(content)
313 except ValueError:
314 content_json = None
315
316 # Buildbucket could return an error even if status==200.
317 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000318 error = content_json.get('error')
319 if error.get('code') == 403:
320 raise BuildbucketResponseException(
321 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000322 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000323 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000324 raise BuildbucketResponseException(msg)
325
326 if response.status == 200:
327 if not content_json:
328 raise BuildbucketResponseException(
329 'Buildbucket returns invalid json content: %s.\n'
330 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
331 content)
332 return content_json
333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content)
335
336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700338 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 try_count += 1
340 assert False, 'unreachable'
341
342
qyearsley1fdfcb62016-10-24 13:22:03 -0700343def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700344 """Returns a dict mapping bucket names to builders and tests,
345 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700346 """
qyearsleydd49f942016-10-28 11:57:22 -0700347 # If no bots are listed, we try to get a set of builders and tests based
348 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700349 if not options.bot:
350 change = changelist.GetChange(
351 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700352 # Get try masters from PRESUBMIT.py files.
353 return presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700354 change=change,
355 changed_files=change.LocalPaths(),
356 repository_root=settings.GetRoot(),
357 default_presubmit=None,
358 project=None,
359 verbose=options.verbose,
360 output_stream=sys.stdout)
361
qyearsley1fdfcb62016-10-24 13:22:03 -0700362 if options.bucket:
363 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700364 if options.master:
365 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700366
qyearsleydd49f942016-10-28 11:57:22 -0700367 # If bots are listed but no master or bucket, then we need to find out
368 # the corresponding master for each bot.
369 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
370 if error_message:
371 option_parser.error(
372 'Tryserver master cannot be found because: %s\n'
373 'Please manually specify the tryserver master, e.g. '
374 '"-m tryserver.chromium.linux".' % error_message)
375 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700376
377
qyearsley123a4682016-10-26 09:12:17 -0700378def _get_bucket_map_for_builders(builders):
379 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700380 map_url = 'https://builders-map.appspot.com/'
381 try:
qyearsley123a4682016-10-26 09:12:17 -0700382 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 except urllib2.URLError as e:
384 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
385 (map_url, e))
386 except ValueError as e:
387 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700388 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 return None, 'Failed to build master map.'
390
qyearsley123a4682016-10-26 09:12:17 -0700391 bucket_map = {}
392 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700393 masters = builders_map.get(builder, [])
394 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700395 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700396 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700397 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700398 (builder, masters))
399 bucket = _prefix_master(masters[0])
400 bucket_map.setdefault(bucket, {})[builder] = []
401
402 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700403
404
borenet6c0efe62016-10-19 08:13:29 -0700405def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700406 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 """Sends a request to Buildbucket to trigger try jobs for a changelist.
408
409 Args:
410 auth_config: AuthConfig for Rietveld.
411 changelist: Changelist that the try jobs are associated with.
412 buckets: A nested dict mapping bucket names to builders to tests.
413 options: Command-line options.
414 """
tandriide281ae2016-10-12 06:02:30 -0700415 assert changelist.GetIssue(), 'CL must be uploaded first'
416 codereview_url = changelist.GetCodereviewServer()
417 assert codereview_url, 'CL must be uploaded first'
418 patchset = patchset or changelist.GetMostRecentPatchset()
419 assert patchset, 'CL must be uploaded first'
420
421 codereview_host = urlparse.urlparse(codereview_url).hostname
422 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000423 http = authenticator.authorize(httplib2.Http())
424 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700425
426 # TODO(tandrii): consider caching Gerrit CL details just like
427 # _RietveldChangelistImpl does, then caching values in these two variables
428 # won't be necessary.
429 owner_email = changelist.GetIssueOwner()
430 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000431
432 buildbucket_put_url = (
433 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000434 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700435 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
436 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
437 hostname=codereview_host,
438 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000439 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700440 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000441
442 batch_req_body = {'builds': []}
443 print_text = []
444 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700445 for bucket, builders_and_tests in sorted(buckets.iteritems()):
446 print_text.append('Bucket: %s' % bucket)
447 master = None
448 if bucket.startswith(MASTER_PREFIX):
449 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000450 for builder, tests in sorted(builders_and_tests.iteritems()):
451 print_text.append(' %s: %s' % (builder, tests))
452 parameters = {
453 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000454 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700455 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000456 'revision': options.revision,
457 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458 'properties': {
459 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700460 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700461 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000462 'patch_storage': 'rietveld',
463 'patchset': patchset,
tandriide281ae2016-10-12 06:02:30 -0700464 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 },
466 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000467 if 'presubmit' in builder.lower():
468 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000469 if tests:
470 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700471 if extra_properties:
472 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000473 if options.clobber:
474 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700475
476 tags = [
477 'builder:%s' % builder,
478 'buildset:%s' % buildset,
479 'user_agent:git_cl_try',
480 ]
481 if master:
482 parameters['properties']['master'] = master
483 tags.append('master:%s' % master)
484
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 batch_req_body['builds'].append(
486 {
487 'bucket': bucket,
488 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000489 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700490 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 }
492 )
493
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700495 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000496 http,
497 buildbucket_put_url,
498 'PUT',
499 body=json.dumps(batch_req_body),
500 headers={'Content-Type': 'application/json'}
501 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000502 print_text.append('To see results here, run: git cl try-results')
503 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700504 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000505
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506
tandrii221ab252016-10-06 08:12:04 -0700507def fetch_try_jobs(auth_config, changelist, buildbucket_host,
508 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700509 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510
qyearsley53f48a12016-09-01 10:45:13 -0700511 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 """
tandrii221ab252016-10-06 08:12:04 -0700513 assert buildbucket_host
514 assert changelist.GetIssue(), 'CL must be uploaded first'
515 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
516 patchset = patchset or changelist.GetMostRecentPatchset()
517 assert patchset, 'CL must be uploaded first'
518
519 codereview_url = changelist.GetCodereviewServer()
520 codereview_host = urlparse.urlparse(codereview_url).hostname
521 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000522 if authenticator.has_cached_credentials():
523 http = authenticator.authorize(httplib2.Http())
524 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700525 print('Warning: Some results might be missing because %s' %
526 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700527 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528 http = httplib2.Http()
529
530 http.force_exception_to_status_code = True
531
tandrii221ab252016-10-06 08:12:04 -0700532 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
533 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
534 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700536 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 params = {'tag': 'buildset:%s' % buildset}
538
539 builds = {}
540 while True:
541 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700542 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700544 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 for build in content.get('builds', []):
546 builds[build['id']] = build
547 if 'next_cursor' in content:
548 params['start_cursor'] = content['next_cursor']
549 else:
550 break
551 return builds
552
553
qyearsleyeab3c042016-08-24 09:18:28 -0700554def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555 """Prints nicely result of fetch_try_jobs."""
556 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700557 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 return
559
560 # Make a copy, because we'll be modifying builds dictionary.
561 builds = builds.copy()
562 builder_names_cache = {}
563
564 def get_builder(b):
565 try:
566 return builder_names_cache[b['id']]
567 except KeyError:
568 try:
569 parameters = json.loads(b['parameters_json'])
570 name = parameters['builder_name']
571 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700572 print('WARNING: failed to get builder name for build %s: %s' % (
573 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 name = None
575 builder_names_cache[b['id']] = name
576 return name
577
578 def get_bucket(b):
579 bucket = b['bucket']
580 if bucket.startswith('master.'):
581 return bucket[len('master.'):]
582 return bucket
583
584 if options.print_master:
585 name_fmt = '%%-%ds %%-%ds' % (
586 max(len(str(get_bucket(b))) for b in builds.itervalues()),
587 max(len(str(get_builder(b))) for b in builds.itervalues()))
588 def get_name(b):
589 return name_fmt % (get_bucket(b), get_builder(b))
590 else:
591 name_fmt = '%%-%ds' % (
592 max(len(str(get_builder(b))) for b in builds.itervalues()))
593 def get_name(b):
594 return name_fmt % get_builder(b)
595
596 def sort_key(b):
597 return b['status'], b.get('result'), get_name(b), b.get('url')
598
599 def pop(title, f, color=None, **kwargs):
600 """Pop matching builds from `builds` dict and print them."""
601
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000602 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000603 colorize = str
604 else:
605 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
606
607 result = []
608 for b in builds.values():
609 if all(b.get(k) == v for k, v in kwargs.iteritems()):
610 builds.pop(b['id'])
611 result.append(b)
612 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700613 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700615 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000616
617 total = len(builds)
618 pop(status='COMPLETED', result='SUCCESS',
619 title='Successes:', color=Fore.GREEN,
620 f=lambda b: (get_name(b), b.get('url')))
621 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
622 title='Infra Failures:', color=Fore.MAGENTA,
623 f=lambda b: (get_name(b), b.get('url')))
624 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
625 title='Failures:', color=Fore.RED,
626 f=lambda b: (get_name(b), b.get('url')))
627 pop(status='COMPLETED', result='CANCELED',
628 title='Canceled:', color=Fore.MAGENTA,
629 f=lambda b: (get_name(b),))
630 pop(status='COMPLETED', result='FAILURE',
631 failure_reason='INVALID_BUILD_DEFINITION',
632 title='Wrong master/builder name:', color=Fore.MAGENTA,
633 f=lambda b: (get_name(b),))
634 pop(status='COMPLETED', result='FAILURE',
635 title='Other failures:',
636 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
637 pop(status='COMPLETED',
638 title='Other finished:',
639 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
640 pop(status='STARTED',
641 title='Started:', color=Fore.YELLOW,
642 f=lambda b: (get_name(b), b.get('url')))
643 pop(status='SCHEDULED',
644 title='Scheduled:',
645 f=lambda b: (get_name(b), 'id=%s' % b['id']))
646 # The last section is just in case buildbucket API changes OR there is a bug.
647 pop(title='Other:',
648 f=lambda b: (get_name(b), 'id=%s' % b['id']))
649 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700650 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000651
652
qyearsley53f48a12016-09-01 10:45:13 -0700653def write_try_results_json(output_file, builds):
654 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
655
656 The input |builds| dict is assumed to be generated by Buildbucket.
657 Buildbucket documentation: http://goo.gl/G0s101
658 """
659
660 def convert_build_dict(build):
661 return {
662 'buildbucket_id': build.get('id'),
663 'status': build.get('status'),
664 'result': build.get('result'),
665 'bucket': build.get('bucket'),
666 'builder_name': json.loads(
667 build.get('parameters_json', '{}')).get('builder_name'),
668 'failure_reason': build.get('failure_reason'),
669 'url': build.get('url'),
670 }
671
672 converted = []
673 for _, build in sorted(builds.items()):
674 converted.append(convert_build_dict(build))
675 write_json(output_file, converted)
676
677
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000678def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
679 """Return the corresponding git ref if |base_url| together with |glob_spec|
680 matches the full |url|.
681
682 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
683 """
684 fetch_suburl, as_ref = glob_spec.split(':')
685 if allow_wildcards:
686 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
687 if glob_match:
688 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
689 # "branches/{472,597,648}/src:refs/remotes/svn/*".
690 branch_re = re.escape(base_url)
691 if glob_match.group(1):
692 branch_re += '/' + re.escape(glob_match.group(1))
693 wildcard = glob_match.group(2)
694 if wildcard == '*':
695 branch_re += '([^/]*)'
696 else:
697 # Escape and replace surrounding braces with parentheses and commas
698 # with pipe symbols.
699 wildcard = re.escape(wildcard)
700 wildcard = re.sub('^\\\\{', '(', wildcard)
701 wildcard = re.sub('\\\\,', '|', wildcard)
702 wildcard = re.sub('\\\\}$', ')', wildcard)
703 branch_re += wildcard
704 if glob_match.group(3):
705 branch_re += re.escape(glob_match.group(3))
706 match = re.match(branch_re, url)
707 if match:
708 return re.sub('\*$', match.group(1), as_ref)
709
710 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
711 if fetch_suburl:
712 full_url = base_url + '/' + fetch_suburl
713 else:
714 full_url = base_url
715 if full_url == url:
716 return as_ref
717 return None
718
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000719
iannucci@chromium.org79540052012-10-19 23:15:26 +0000720def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000721 """Prints statistics about the change to the user."""
722 # --no-ext-diff is broken in some versions of Git, so try to work around
723 # this by overriding the environment (but there is still a problem if the
724 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000725 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000726 if 'GIT_EXTERNAL_DIFF' in env:
727 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000728
729 if find_copies:
730 similarity_options = ['--find-copies-harder', '-l100000',
731 '-C%s' % similarity]
732 else:
733 similarity_options = ['-M%s' % similarity]
734
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000735 try:
736 stdout = sys.stdout.fileno()
737 except AttributeError:
738 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000739 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000740 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000741 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000742 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000743
744
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000745class BuildbucketResponseException(Exception):
746 pass
747
748
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749class Settings(object):
750 def __init__(self):
751 self.default_server = None
752 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000753 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 self.is_git_svn = None
755 self.svn_branch = None
756 self.tree_status_url = None
757 self.viewvc_url = None
758 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000759 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000760 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000761 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000762 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000763 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000764 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000765 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700766 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767
768 def LazyUpdateIfNeeded(self):
769 """Updates the settings from a codereview.settings file, if available."""
770 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000771 # The only value that actually changes the behavior is
772 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000773 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000774 error_ok=True
775 ).strip().lower()
776
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000778 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 LoadCodereviewSettingsFromFile(cr_settings_file)
780 self.updated = True
781
782 def GetDefaultServerUrl(self, error_ok=False):
783 if not self.default_server:
784 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000785 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 if error_ok:
788 return self.default_server
789 if not self.default_server:
790 error_message = ('Could not find settings file. You must configure '
791 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000792 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 return self.default_server
795
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000796 @staticmethod
797 def GetRelativeRoot():
798 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000801 if self.root is None:
802 self.root = os.path.abspath(self.GetRelativeRoot())
803 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000805 def GetGitMirror(self, remote='origin'):
806 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000807 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000808 if not os.path.isdir(local_url):
809 return None
810 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
811 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
812 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
813 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
814 if mirror.exists():
815 return mirror
816 return None
817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 def GetIsGitSvn(self):
819 """Return true if this repo looks like it's using git-svn."""
820 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000821 if self.GetPendingRefPrefix():
822 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
823 self.is_git_svn = False
824 else:
825 # If you have any "svn-remote.*" config keys, we think you're using svn.
826 self.is_git_svn = RunGitWithCode(
827 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 return self.is_git_svn
829
830 def GetSVNBranch(self):
831 if self.svn_branch is None:
832 if not self.GetIsGitSvn():
833 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
834
835 # Try to figure out which remote branch we're based on.
836 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000837 # 1) iterate through our branch history and find the svn URL.
838 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839
840 # regexp matching the git-svn line that contains the URL.
841 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
842
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000843 # We don't want to go through all of history, so read a line from the
844 # pipe at a time.
845 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000846 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
848 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000849 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000850 for line in proc.stdout:
851 match = git_svn_re.match(line)
852 if match:
853 url = match.group(1)
854 proc.stdout.close() # Cut pipe.
855 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000857 if url:
858 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
859 remotes = RunGit(['config', '--get-regexp',
860 r'^svn-remote\..*\.url']).splitlines()
861 for remote in remotes:
862 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000864 remote = match.group(1)
865 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000866 rewrite_root = RunGit(
867 ['config', 'svn-remote.%s.rewriteRoot' % remote],
868 error_ok=True).strip()
869 if rewrite_root:
870 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000871 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000872 ['config', 'svn-remote.%s.fetch' % remote],
873 error_ok=True).strip()
874 if fetch_spec:
875 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
876 if self.svn_branch:
877 break
878 branch_spec = RunGit(
879 ['config', 'svn-remote.%s.branches' % remote],
880 error_ok=True).strip()
881 if branch_spec:
882 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
883 if self.svn_branch:
884 break
885 tag_spec = RunGit(
886 ['config', 'svn-remote.%s.tags' % remote],
887 error_ok=True).strip()
888 if tag_spec:
889 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
890 if self.svn_branch:
891 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892
893 if not self.svn_branch:
894 DieWithError('Can\'t guess svn branch -- try specifying it on the '
895 'command line')
896
897 return self.svn_branch
898
899 def GetTreeStatusUrl(self, error_ok=False):
900 if not self.tree_status_url:
901 error_message = ('You must configure your tree status URL by running '
902 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000903 self.tree_status_url = self._GetRietveldConfig(
904 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 return self.tree_status_url
906
907 def GetViewVCUrl(self):
908 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000909 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 return self.viewvc_url
911
rmistry@google.com90752582014-01-14 21:04:50 +0000912 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000913 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000914
rmistry@google.com78948ed2015-07-08 23:09:57 +0000915 def GetIsSkipDependencyUpload(self, branch_name):
916 """Returns true if specified branch should skip dep uploads."""
917 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
918 error_ok=True)
919
rmistry@google.com5626a922015-02-26 14:03:30 +0000920 def GetRunPostUploadHook(self):
921 run_post_upload_hook = self._GetRietveldConfig(
922 'run-post-upload-hook', error_ok=True)
923 return run_post_upload_hook == "True"
924
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000925 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000926 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000927
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000928 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000929 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000930
ukai@chromium.orge8077812012-02-03 03:41:46 +0000931 def GetIsGerrit(self):
932 """Return true if this repo is assosiated with gerrit code review system."""
933 if self.is_gerrit is None:
934 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
935 return self.is_gerrit
936
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000937 def GetSquashGerritUploads(self):
938 """Return true if uploads to Gerrit should be squashed by default."""
939 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700940 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
941 if self.squash_gerrit_uploads is None:
942 # Default is squash now (http://crbug.com/611892#c23).
943 self.squash_gerrit_uploads = not (
944 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
945 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000946 return self.squash_gerrit_uploads
947
tandriia60502f2016-06-20 02:01:53 -0700948 def GetSquashGerritUploadsOverride(self):
949 """Return True or False if codereview.settings should be overridden.
950
951 Returns None if no override has been defined.
952 """
953 # See also http://crbug.com/611892#c23
954 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
955 error_ok=True).strip()
956 if result == 'true':
957 return True
958 if result == 'false':
959 return False
960 return None
961
tandrii@chromium.org28253532016-04-14 13:46:56 +0000962 def GetGerritSkipEnsureAuthenticated(self):
963 """Return True if EnsureAuthenticated should not be done for Gerrit
964 uploads."""
965 if self.gerrit_skip_ensure_authenticated is None:
966 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000967 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000968 error_ok=True).strip() == 'true')
969 return self.gerrit_skip_ensure_authenticated
970
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000971 def GetGitEditor(self):
972 """Return the editor specified in the git config, or None if none is."""
973 if self.git_editor is None:
974 self.git_editor = self._GetConfig('core.editor', error_ok=True)
975 return self.git_editor or None
976
thestig@chromium.org44202a22014-03-11 19:22:18 +0000977 def GetLintRegex(self):
978 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
979 DEFAULT_LINT_REGEX)
980
981 def GetLintIgnoreRegex(self):
982 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
983 DEFAULT_LINT_IGNORE_REGEX)
984
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000985 def GetProject(self):
986 if not self.project:
987 self.project = self._GetRietveldConfig('project', error_ok=True)
988 return self.project
989
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000990 def GetForceHttpsCommitUrl(self):
991 if not self.force_https_commit_url:
992 self.force_https_commit_url = self._GetRietveldConfig(
993 'force-https-commit-url', error_ok=True)
994 return self.force_https_commit_url
995
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000996 def GetPendingRefPrefix(self):
997 if not self.pending_ref_prefix:
998 self.pending_ref_prefix = self._GetRietveldConfig(
999 'pending-ref-prefix', error_ok=True)
1000 return self.pending_ref_prefix
1001
tandriif46c20f2016-09-14 06:17:05 -07001002 def GetHasGitNumberFooter(self):
1003 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1004 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1005 if not self.git_number_footer:
1006 self.git_number_footer = self._GetRietveldConfig(
1007 'git-number-footer', error_ok=True)
1008 return self.git_number_footer
1009
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001010 def _GetRietveldConfig(self, param, **kwargs):
1011 return self._GetConfig('rietveld.' + param, **kwargs)
1012
rmistry@google.com78948ed2015-07-08 23:09:57 +00001013 def _GetBranchConfig(self, branch_name, param, **kwargs):
1014 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 def _GetConfig(self, param, **kwargs):
1017 self.LazyUpdateIfNeeded()
1018 return RunGit(['config', param], **kwargs).strip()
1019
1020
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021def ShortBranchName(branch):
1022 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001023 return branch.replace('refs/heads/', '', 1)
1024
1025
1026def GetCurrentBranchRef():
1027 """Returns branch ref (e.g., refs/heads/master) or None."""
1028 return RunGit(['symbolic-ref', 'HEAD'],
1029 stderr=subprocess2.VOID, error_ok=True).strip() or None
1030
1031
1032def GetCurrentBranch():
1033 """Returns current branch or None.
1034
1035 For refs/heads/* branches, returns just last part. For others, full ref.
1036 """
1037 branchref = GetCurrentBranchRef()
1038 if branchref:
1039 return ShortBranchName(branchref)
1040 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041
1042
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001043class _CQState(object):
1044 """Enum for states of CL with respect to Commit Queue."""
1045 NONE = 'none'
1046 DRY_RUN = 'dry_run'
1047 COMMIT = 'commit'
1048
1049 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1050
1051
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001052class _ParsedIssueNumberArgument(object):
1053 def __init__(self, issue=None, patchset=None, hostname=None):
1054 self.issue = issue
1055 self.patchset = patchset
1056 self.hostname = hostname
1057
1058 @property
1059 def valid(self):
1060 return self.issue is not None
1061
1062
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001063def ParseIssueNumberArgument(arg):
1064 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1065 fail_result = _ParsedIssueNumberArgument()
1066
1067 if arg.isdigit():
1068 return _ParsedIssueNumberArgument(issue=int(arg))
1069 if not arg.startswith('http'):
1070 return fail_result
1071 url = gclient_utils.UpgradeToHttps(arg)
1072 try:
1073 parsed_url = urlparse.urlparse(url)
1074 except ValueError:
1075 return fail_result
1076 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1077 tmp = cls.ParseIssueURL(parsed_url)
1078 if tmp is not None:
1079 return tmp
1080 return fail_result
1081
1082
tandriic2405f52016-10-10 08:13:15 -07001083class GerritIssueNotExists(Exception):
1084 def __init__(self, issue, url):
1085 self.issue = issue
1086 self.url = url
1087 super(GerritIssueNotExists, self).__init__()
1088
1089 def __str__(self):
1090 return 'issue %s at %s does not exist or you have no access to it' % (
1091 self.issue, self.url)
1092
1093
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001095 """Changelist works with one changelist in local branch.
1096
1097 Supports two codereview backends: Rietveld or Gerrit, selected at object
1098 creation.
1099
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001100 Notes:
1101 * Not safe for concurrent multi-{thread,process} use.
1102 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001103 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 """
1105
1106 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1107 """Create a new ChangeList instance.
1108
1109 If issue is given, the codereview must be given too.
1110
1111 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1112 Otherwise, it's decided based on current configuration of the local branch,
1113 with default being 'rietveld' for backwards compatibility.
1114 See _load_codereview_impl for more details.
1115
1116 **kwargs will be passed directly to codereview implementation.
1117 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001119 global settings
1120 if not settings:
1121 # Happens when git_cl.py is used as a utility library.
1122 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001123
1124 if issue:
1125 assert codereview, 'codereview must be known, if issue is known'
1126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 self.branchref = branchref
1128 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001129 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 self.branch = ShortBranchName(self.branchref)
1131 else:
1132 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001134 self.lookedup_issue = False
1135 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 self.has_description = False
1137 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001140 self.cc = None
1141 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001142 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001143
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001146 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001147 assert self._codereview_impl
1148 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149
1150 def _load_codereview_impl(self, codereview=None, **kwargs):
1151 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001152 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1153 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1154 self._codereview = codereview
1155 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 return
1157
1158 # Automatic selection based on issue number set for a current branch.
1159 # Rietveld takes precedence over Gerrit.
1160 assert not self.issue
1161 # Whether we find issue or not, we are doing the lookup.
1162 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001163 if self.GetBranch():
1164 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1165 issue = _git_get_branch_config_value(
1166 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1167 if issue:
1168 self._codereview = codereview
1169 self._codereview_impl = cls(self, **kwargs)
1170 self.issue = int(issue)
1171 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172
1173 # No issue is set for this branch, so decide based on repo-wide settings.
1174 return self._load_codereview_impl(
1175 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1176 **kwargs)
1177
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001178 def IsGerrit(self):
1179 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001180
1181 def GetCCList(self):
1182 """Return the users cc'd on this CL.
1183
agable92bec4f2016-08-24 09:27:27 -07001184 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001185 """
1186 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001187 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001188 more_cc = ','.join(self.watchers)
1189 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1190 return self.cc
1191
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001192 def GetCCListWithoutDefault(self):
1193 """Return the users cc'd on this CL excluding default ones."""
1194 if self.cc is None:
1195 self.cc = ','.join(self.watchers)
1196 return self.cc
1197
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 def SetWatchers(self, watchers):
1199 """Set the list of email addresses that should be cc'd based on the changed
1200 files in this CL.
1201 """
1202 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203
1204 def GetBranch(self):
1205 """Returns the short branch name, e.g. 'master'."""
1206 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001207 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001208 if not branchref:
1209 return None
1210 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 self.branch = ShortBranchName(self.branchref)
1212 return self.branch
1213
1214 def GetBranchRef(self):
1215 """Returns the full branch name, e.g. 'refs/heads/master'."""
1216 self.GetBranch() # Poke the lazy loader.
1217 return self.branchref
1218
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001219 def ClearBranch(self):
1220 """Clears cached branch data of this object."""
1221 self.branch = self.branchref = None
1222
tandrii5d48c322016-08-18 16:19:37 -07001223 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1224 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1225 kwargs['branch'] = self.GetBranch()
1226 return _git_get_branch_config_value(key, default, **kwargs)
1227
1228 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1229 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1230 assert self.GetBranch(), (
1231 'this CL must have an associated branch to %sset %s%s' %
1232 ('un' if value is None else '',
1233 key,
1234 '' if value is None else ' to %r' % value))
1235 kwargs['branch'] = self.GetBranch()
1236 return _git_set_branch_config_value(key, value, **kwargs)
1237
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001238 @staticmethod
1239 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001240 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 e.g. 'origin', 'refs/heads/master'
1242 """
1243 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001244 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1245
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001247 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001249 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1250 error_ok=True).strip()
1251 if upstream_branch:
1252 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001254 # Fall back on trying a git-svn upstream branch.
1255 if settings.GetIsGitSvn():
1256 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001258 # Else, try to guess the origin remote.
1259 remote_branches = RunGit(['branch', '-r']).split()
1260 if 'origin/master' in remote_branches:
1261 # Fall back on origin/master if it exits.
1262 remote = 'origin'
1263 upstream_branch = 'refs/heads/master'
1264 elif 'origin/trunk' in remote_branches:
1265 # Fall back on origin/trunk if it exists. Generally a shared
1266 # git-svn clone
1267 remote = 'origin'
1268 upstream_branch = 'refs/heads/trunk'
1269 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001270 DieWithError(
1271 'Unable to determine default branch to diff against.\n'
1272 'Either pass complete "git diff"-style arguments, like\n'
1273 ' git cl upload origin/master\n'
1274 'or verify this branch is set up to track another \n'
1275 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276
1277 return remote, upstream_branch
1278
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001279 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001280 upstream_branch = self.GetUpstreamBranch()
1281 if not BranchExists(upstream_branch):
1282 DieWithError('The upstream for the current branch (%s) does not exist '
1283 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001284 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001285 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 def GetUpstreamBranch(self):
1288 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001289 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001291 upstream_branch = upstream_branch.replace('refs/heads/',
1292 'refs/remotes/%s/' % remote)
1293 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1294 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 self.upstream_branch = upstream_branch
1296 return self.upstream_branch
1297
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001299 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, branch = None, self.GetBranch()
1301 seen_branches = set()
1302 while branch not in seen_branches:
1303 seen_branches.add(branch)
1304 remote, branch = self.FetchUpstreamTuple(branch)
1305 branch = ShortBranchName(branch)
1306 if remote != '.' or branch.startswith('refs/remotes'):
1307 break
1308 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309 remotes = RunGit(['remote'], error_ok=True).split()
1310 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 logging.warning('Could not determine which remote this change is '
1315 'associated with, so defaulting to "%s". This may '
1316 'not be what you want. You may prevent this message '
1317 'by running "git svn info" as documented here: %s',
1318 self._remote,
1319 GIT_INSTRUCTIONS_URL)
1320 else:
1321 logging.warn('Could not determine which remote this change is '
1322 'associated with. You may prevent this message by '
1323 'running "git svn info" as documented here: %s',
1324 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001325 branch = 'HEAD'
1326 if branch.startswith('refs/remotes'):
1327 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001328 elif branch.startswith('refs/branch-heads/'):
1329 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 else:
1331 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001332 return self._remote
1333
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001334 def GitSanityChecks(self, upstream_git_obj):
1335 """Checks git repo status and ensures diff is from local commits."""
1336
sbc@chromium.org79706062015-01-14 21:18:12 +00001337 if upstream_git_obj is None:
1338 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001339 print('ERROR: unable to determine current branch (detached HEAD?)',
1340 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001341 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001342 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001343 return False
1344
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 # Verify the commit we're diffing against is in our current branch.
1346 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1347 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1348 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001349 print('ERROR: %s is not in the current branch. You may need to rebase '
1350 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 return False
1352
1353 # List the commits inside the diff, and verify they are all local.
1354 commits_in_diff = RunGit(
1355 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1356 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1357 remote_branch = remote_branch.strip()
1358 if code != 0:
1359 _, remote_branch = self.GetRemoteBranch()
1360
1361 commits_in_remote = RunGit(
1362 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1363
1364 common_commits = set(commits_in_diff) & set(commits_in_remote)
1365 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001366 print('ERROR: Your diff contains %d commits already in %s.\n'
1367 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1368 'the diff. If you are using a custom git flow, you can override'
1369 ' the reference used for this check with "git config '
1370 'gitcl.remotebranch <git-ref>".' % (
1371 len(common_commits), remote_branch, upstream_git_obj),
1372 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001373 return False
1374 return True
1375
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001376 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001377 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001378
1379 Returns None if it is not set.
1380 """
tandrii5d48c322016-08-18 16:19:37 -07001381 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001382
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001383 def GetGitSvnRemoteUrl(self):
1384 """Return the configured git-svn remote URL parsed from git svn info.
1385
1386 Returns None if it is not set.
1387 """
1388 # URL is dependent on the current directory.
1389 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1390 if data:
1391 keys = dict(line.split(': ', 1) for line in data.splitlines()
1392 if ': ' in line)
1393 return keys.get('URL', None)
1394 return None
1395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 def GetRemoteUrl(self):
1397 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1398
1399 Returns None if there is no remote.
1400 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001401 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001402 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1403
1404 # If URL is pointing to a local directory, it is probably a git cache.
1405 if os.path.isdir(url):
1406 url = RunGit(['config', 'remote.%s.url' % remote],
1407 error_ok=True,
1408 cwd=url).strip()
1409 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001411 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001412 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001413 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001414 self.issue = self._GitGetBranchConfigValue(
1415 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001416 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 return self.issue
1418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 def GetIssueURL(self):
1420 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421 issue = self.GetIssue()
1422 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001423 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425
1426 def GetDescription(self, pretty=False):
1427 if not self.has_description:
1428 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001429 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 self.has_description = True
1431 if pretty:
1432 wrapper = textwrap.TextWrapper()
1433 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1434 return wrapper.fill(self.description)
1435 return self.description
1436
1437 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001440 self.patchset = self._GitGetBranchConfigValue(
1441 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.patchset
1444
1445 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001446 """Set this branch's patchset. If patchset=0, clears the patchset."""
1447 assert self.GetBranch()
1448 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001450 else:
1451 self.patchset = int(patchset)
1452 self._GitSetBranchConfigValue(
1453 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001455 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001456 """Set this branch's issue. If issue isn't given, clears the issue."""
1457 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001459 issue = int(issue)
1460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001462 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 codereview_server = self._codereview_impl.GetCodereviewServer()
1464 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001465 self._GitSetBranchConfigValue(
1466 self._codereview_impl.CodereviewServerConfigKey(),
1467 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 else:
tandrii5d48c322016-08-18 16:19:37 -07001469 # Reset all of these just to be clean.
1470 reset_suffixes = [
1471 'last-upload-hash',
1472 self._codereview_impl.IssueConfigKey(),
1473 self._codereview_impl.PatchsetConfigKey(),
1474 self._codereview_impl.CodereviewServerConfigKey(),
1475 ] + self._PostUnsetIssueProperties()
1476 for prop in reset_suffixes:
1477 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001478 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001479 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480
dnjba1b0f32016-09-02 12:37:42 -07001481 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001482 if not self.GitSanityChecks(upstream_branch):
1483 DieWithError('\nGit sanity check failure')
1484
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001486 if not root:
1487 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001488 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
1490 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001491 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001492 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001494 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001495 except subprocess2.CalledProcessError:
1496 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001497 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001498 'This branch probably doesn\'t exist anymore. To reset the\n'
1499 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001500 ' git branch --set-upstream-to origin/master %s\n'
1501 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001502 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001503
maruel@chromium.org52424302012-08-29 15:14:30 +00001504 issue = self.GetIssue()
1505 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001506 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001507 description = self.GetDescription()
1508 else:
1509 # If the change was never uploaded, use the log messages of all commits
1510 # up to the branch point, as git cl upload will prefill the description
1511 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1513 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001514
1515 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001516 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001517 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001518 name,
1519 description,
1520 absroot,
1521 files,
1522 issue,
1523 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001524 author,
1525 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001526
dsansomee2d6fd92016-09-08 00:10:47 -07001527 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001528 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001529 return self._codereview_impl.UpdateDescriptionRemote(
1530 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001531
1532 def RunHook(self, committing, may_prompt, verbose, change):
1533 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1534 try:
1535 return presubmit_support.DoPresubmitChecks(change, committing,
1536 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1537 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001538 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1539 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001540 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001541 DieWithError(
1542 ('%s\nMaybe your depot_tools is out of date?\n'
1543 'If all fails, contact maruel@') % e)
1544
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001545 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1546 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001547 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1548 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001549 else:
1550 # Assume url.
1551 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1552 urlparse.urlparse(issue_arg))
1553 if not parsed_issue_arg or not parsed_issue_arg.valid:
1554 DieWithError('Failed to parse issue argument "%s". '
1555 'Must be an issue number or a valid URL.' % issue_arg)
1556 return self._codereview_impl.CMDPatchWithParsedIssue(
1557 parsed_issue_arg, reject, nocommit, directory)
1558
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001559 def CMDUpload(self, options, git_diff_args, orig_args):
1560 """Uploads a change to codereview."""
1561 if git_diff_args:
1562 # TODO(ukai): is it ok for gerrit case?
1563 base_branch = git_diff_args[0]
1564 else:
1565 if self.GetBranch() is None:
1566 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1567
1568 # Default to diffing against common ancestor of upstream branch
1569 base_branch = self.GetCommonAncestorWithUpstream()
1570 git_diff_args = [base_branch, 'HEAD']
1571
1572 # Make sure authenticated to codereview before running potentially expensive
1573 # hooks. It is a fast, best efforts check. Codereview still can reject the
1574 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001575 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576
1577 # Apply watchlists on upload.
1578 change = self.GetChange(base_branch, None)
1579 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1580 files = [f.LocalPath() for f in change.AffectedFiles()]
1581 if not options.bypass_watchlists:
1582 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1583
1584 if not options.bypass_hooks:
1585 if options.reviewers or options.tbr_owners:
1586 # Set the reviewer list now so that presubmit checks can access it.
1587 change_description = ChangeDescription(change.FullDescriptionText())
1588 change_description.update_reviewers(options.reviewers,
1589 options.tbr_owners,
1590 change)
1591 change.SetDescriptionText(change_description.description)
1592 hook_results = self.RunHook(committing=False,
1593 may_prompt=not options.force,
1594 verbose=options.verbose,
1595 change=change)
1596 if not hook_results.should_continue():
1597 return 1
1598 if not options.reviewers and hook_results.reviewers:
1599 options.reviewers = hook_results.reviewers.split(',')
1600
1601 if self.GetIssue():
1602 latest_patchset = self.GetMostRecentPatchset()
1603 local_patchset = self.GetPatchset()
1604 if (latest_patchset and local_patchset and
1605 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001606 print('The last upload made from this repository was patchset #%d but '
1607 'the most recent patchset on the server is #%d.'
1608 % (local_patchset, latest_patchset))
1609 print('Uploading will still work, but if you\'ve uploaded to this '
1610 'issue from another machine or branch the patch you\'re '
1611 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001612 ask_for_data('About to upload; enter to confirm.')
1613
1614 print_stats(options.similarity, options.find_copies, git_diff_args)
1615 ret = self.CMDUploadChange(options, git_diff_args, change)
1616 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001617 if options.use_commit_queue:
1618 self.SetCQState(_CQState.COMMIT)
1619 elif options.cq_dry_run:
1620 self.SetCQState(_CQState.DRY_RUN)
1621
tandrii5d48c322016-08-18 16:19:37 -07001622 _git_set_branch_config_value('last-upload-hash',
1623 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001624 # Run post upload hooks, if specified.
1625 if settings.GetRunPostUploadHook():
1626 presubmit_support.DoPostUploadExecuter(
1627 change,
1628 self,
1629 settings.GetRoot(),
1630 options.verbose,
1631 sys.stdout)
1632
1633 # Upload all dependencies if specified.
1634 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001635 print()
1636 print('--dependencies has been specified.')
1637 print('All dependent local branches will be re-uploaded.')
1638 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 # Remove the dependencies flag from args so that we do not end up in a
1640 # loop.
1641 orig_args.remove('--dependencies')
1642 ret = upload_branch_deps(self, orig_args)
1643 return ret
1644
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001645 def SetCQState(self, new_state):
1646 """Update the CQ state for latest patchset.
1647
1648 Issue must have been already uploaded and known.
1649 """
1650 assert new_state in _CQState.ALL_STATES
1651 assert self.GetIssue()
1652 return self._codereview_impl.SetCQState(new_state)
1653
qyearsley1fdfcb62016-10-24 13:22:03 -07001654 def TriggerDryRun(self):
1655 """Triggers a dry run and prints a warning on failure."""
1656 # TODO(qyearsley): Either re-use this method in CMDset_commit
1657 # and CMDupload, or change CMDtry to trigger dry runs with
1658 # just SetCQState, and catch keyboard interrupt and other
1659 # errors in that method.
1660 try:
1661 self.SetCQState(_CQState.DRY_RUN)
1662 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1663 return 0
1664 except KeyboardInterrupt:
1665 raise
1666 except:
1667 print('WARNING: failed to trigger CQ Dry Run.\n'
1668 'Either:\n'
1669 ' * your project has no CQ\n'
1670 ' * you don\'t have permission to trigger Dry Run\n'
1671 ' * bug in this code (see stack trace below).\n'
1672 'Consider specifying which bots to trigger manually '
1673 'or asking your project owners for permissions '
1674 'or contacting Chrome Infrastructure team at '
1675 'https://www.chromium.org/infra\n\n')
1676 # Still raise exception so that stack trace is printed.
1677 raise
1678
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001679 # Forward methods to codereview specific implementation.
1680
1681 def CloseIssue(self):
1682 return self._codereview_impl.CloseIssue()
1683
1684 def GetStatus(self):
1685 return self._codereview_impl.GetStatus()
1686
1687 def GetCodereviewServer(self):
1688 return self._codereview_impl.GetCodereviewServer()
1689
tandriide281ae2016-10-12 06:02:30 -07001690 def GetIssueOwner(self):
1691 """Get owner from codereview, which may differ from this checkout."""
1692 return self._codereview_impl.GetIssueOwner()
1693
1694 def GetIssueProject(self):
1695 """Get project from codereview, which may differ from what this
1696 checkout's codereview.settings or gerrit project URL say.
1697 """
1698 return self._codereview_impl.GetIssueProject()
1699
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001700 def GetApprovingReviewers(self):
1701 return self._codereview_impl.GetApprovingReviewers()
1702
1703 def GetMostRecentPatchset(self):
1704 return self._codereview_impl.GetMostRecentPatchset()
1705
tandriide281ae2016-10-12 06:02:30 -07001706 def CannotTriggerTryJobReason(self):
1707 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1708 return self._codereview_impl.CannotTriggerTryJobReason()
1709
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001710 def __getattr__(self, attr):
1711 # This is because lots of untested code accesses Rietveld-specific stuff
1712 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001713 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001714 # Note that child method defines __getattr__ as well, and forwards it here,
1715 # because _RietveldChangelistImpl is not cleaned up yet, and given
1716 # deprecation of Rietveld, it should probably be just removed.
1717 # Until that time, avoid infinite recursion by bypassing __getattr__
1718 # of implementation class.
1719 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001720
1721
1722class _ChangelistCodereviewBase(object):
1723 """Abstract base class encapsulating codereview specifics of a changelist."""
1724 def __init__(self, changelist):
1725 self._changelist = changelist # instance of Changelist
1726
1727 def __getattr__(self, attr):
1728 # Forward methods to changelist.
1729 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1730 # _RietveldChangelistImpl to avoid this hack?
1731 return getattr(self._changelist, attr)
1732
1733 def GetStatus(self):
1734 """Apply a rough heuristic to give a simple summary of an issue's review
1735 or CQ status, assuming adherence to a common workflow.
1736
1737 Returns None if no issue for this branch, or specific string keywords.
1738 """
1739 raise NotImplementedError()
1740
1741 def GetCodereviewServer(self):
1742 """Returns server URL without end slash, like "https://codereview.com"."""
1743 raise NotImplementedError()
1744
1745 def FetchDescription(self):
1746 """Fetches and returns description from the codereview server."""
1747 raise NotImplementedError()
1748
tandrii5d48c322016-08-18 16:19:37 -07001749 @classmethod
1750 def IssueConfigKey(cls):
1751 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 raise NotImplementedError()
1753
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001754 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001755 def PatchsetConfigKey(cls):
1756 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001757 raise NotImplementedError()
1758
tandrii5d48c322016-08-18 16:19:37 -07001759 @classmethod
1760 def CodereviewServerConfigKey(cls):
1761 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762 raise NotImplementedError()
1763
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001764 def _PostUnsetIssueProperties(self):
1765 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001766 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001767
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001768 def GetRieveldObjForPresubmit(self):
1769 # This is an unfortunate Rietveld-embeddedness in presubmit.
1770 # For non-Rietveld codereviews, this probably should return a dummy object.
1771 raise NotImplementedError()
1772
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001773 def GetGerritObjForPresubmit(self):
1774 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1775 return None
1776
dsansomee2d6fd92016-09-08 00:10:47 -07001777 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001778 """Update the description on codereview site."""
1779 raise NotImplementedError()
1780
1781 def CloseIssue(self):
1782 """Closes the issue."""
1783 raise NotImplementedError()
1784
1785 def GetApprovingReviewers(self):
1786 """Returns a list of reviewers approving the change.
1787
1788 Note: not necessarily committers.
1789 """
1790 raise NotImplementedError()
1791
1792 def GetMostRecentPatchset(self):
1793 """Returns the most recent patchset number from the codereview site."""
1794 raise NotImplementedError()
1795
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001796 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1797 directory):
1798 """Fetches and applies the issue.
1799
1800 Arguments:
1801 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1802 reject: if True, reject the failed patch instead of switching to 3-way
1803 merge. Rietveld only.
1804 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1805 only.
1806 directory: switch to directory before applying the patch. Rietveld only.
1807 """
1808 raise NotImplementedError()
1809
1810 @staticmethod
1811 def ParseIssueURL(parsed_url):
1812 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1813 failed."""
1814 raise NotImplementedError()
1815
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001816 def EnsureAuthenticated(self, force):
1817 """Best effort check that user is authenticated with codereview server.
1818
1819 Arguments:
1820 force: whether to skip confirmation questions.
1821 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001822 raise NotImplementedError()
1823
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001824 def CMDUploadChange(self, options, args, change):
1825 """Uploads a change to codereview."""
1826 raise NotImplementedError()
1827
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001828 def SetCQState(self, new_state):
1829 """Update the CQ state for latest patchset.
1830
1831 Issue must have been already uploaded and known.
1832 """
1833 raise NotImplementedError()
1834
tandriie113dfd2016-10-11 10:20:12 -07001835 def CannotTriggerTryJobReason(self):
1836 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1837 raise NotImplementedError()
1838
tandriide281ae2016-10-12 06:02:30 -07001839 def GetIssueOwner(self):
1840 raise NotImplementedError()
1841
1842 def GetIssueProject(self):
1843 raise NotImplementedError()
1844
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845
1846class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1847 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1848 super(_RietveldChangelistImpl, self).__init__(changelist)
1849 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001850 if not rietveld_server:
1851 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852
1853 self._rietveld_server = rietveld_server
1854 self._auth_config = auth_config
1855 self._props = None
1856 self._rpc_server = None
1857
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 def GetCodereviewServer(self):
1859 if not self._rietveld_server:
1860 # If we're on a branch then get the server potentially associated
1861 # with that branch.
1862 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001863 self._rietveld_server = gclient_utils.UpgradeToHttps(
1864 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865 if not self._rietveld_server:
1866 self._rietveld_server = settings.GetDefaultServerUrl()
1867 return self._rietveld_server
1868
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001869 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001870 """Best effort check that user is authenticated with Rietveld server."""
1871 if self._auth_config.use_oauth2:
1872 authenticator = auth.get_authenticator_for_host(
1873 self.GetCodereviewServer(), self._auth_config)
1874 if not authenticator.has_cached_credentials():
1875 raise auth.LoginRequiredError(self.GetCodereviewServer())
1876
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 def FetchDescription(self):
1878 issue = self.GetIssue()
1879 assert issue
1880 try:
1881 return self.RpcServer().get_description(issue).strip()
1882 except urllib2.HTTPError as e:
1883 if e.code == 404:
1884 DieWithError(
1885 ('\nWhile fetching the description for issue %d, received a '
1886 '404 (not found)\n'
1887 'error. It is likely that you deleted this '
1888 'issue on the server. If this is the\n'
1889 'case, please run\n\n'
1890 ' git cl issue 0\n\n'
1891 'to clear the association with the deleted issue. Then run '
1892 'this command again.') % issue)
1893 else:
1894 DieWithError(
1895 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1896 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001897 print('Warning: Failed to retrieve CL description due to network '
1898 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001899 return ''
1900
1901 def GetMostRecentPatchset(self):
1902 return self.GetIssueProperties()['patchsets'][-1]
1903
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904 def GetIssueProperties(self):
1905 if self._props is None:
1906 issue = self.GetIssue()
1907 if not issue:
1908 self._props = {}
1909 else:
1910 self._props = self.RpcServer().get_issue_properties(issue, True)
1911 return self._props
1912
tandriie113dfd2016-10-11 10:20:12 -07001913 def CannotTriggerTryJobReason(self):
1914 props = self.GetIssueProperties()
1915 if not props:
1916 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1917 if props.get('closed'):
1918 return 'CL %s is closed' % self.GetIssue()
1919 if props.get('private'):
1920 return 'CL %s is private' % self.GetIssue()
1921 return None
1922
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001923 def GetApprovingReviewers(self):
1924 return get_approving_reviewers(self.GetIssueProperties())
1925
tandriide281ae2016-10-12 06:02:30 -07001926 def GetIssueOwner(self):
1927 return (self.GetIssueProperties() or {}).get('owner_email')
1928
1929 def GetIssueProject(self):
1930 return (self.GetIssueProperties() or {}).get('project')
1931
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 def AddComment(self, message):
1933 return self.RpcServer().add_comment(self.GetIssue(), message)
1934
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001935 def GetStatus(self):
1936 """Apply a rough heuristic to give a simple summary of an issue's review
1937 or CQ status, assuming adherence to a common workflow.
1938
1939 Returns None if no issue for this branch, or one of the following keywords:
1940 * 'error' - error from review tool (including deleted issues)
1941 * 'unsent' - not sent for review
1942 * 'waiting' - waiting for review
1943 * 'reply' - waiting for owner to reply to review
1944 * 'lgtm' - LGTM from at least one approved reviewer
1945 * 'commit' - in the commit queue
1946 * 'closed' - closed
1947 """
1948 if not self.GetIssue():
1949 return None
1950
1951 try:
1952 props = self.GetIssueProperties()
1953 except urllib2.HTTPError:
1954 return 'error'
1955
1956 if props.get('closed'):
1957 # Issue is closed.
1958 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001959 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001960 # Issue is in the commit queue.
1961 return 'commit'
1962
1963 try:
1964 reviewers = self.GetApprovingReviewers()
1965 except urllib2.HTTPError:
1966 return 'error'
1967
1968 if reviewers:
1969 # Was LGTM'ed.
1970 return 'lgtm'
1971
1972 messages = props.get('messages') or []
1973
tandrii9d2c7a32016-06-22 03:42:45 -07001974 # Skip CQ messages that don't require owner's action.
1975 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1976 if 'Dry run:' in messages[-1]['text']:
1977 messages.pop()
1978 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1979 # This message always follows prior messages from CQ,
1980 # so skip this too.
1981 messages.pop()
1982 else:
1983 # This is probably a CQ messages warranting user attention.
1984 break
1985
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001986 if not messages:
1987 # No message was sent.
1988 return 'unsent'
1989 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001990 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001991 return 'reply'
1992 return 'waiting'
1993
dsansomee2d6fd92016-09-08 00:10:47 -07001994 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001995 return self.RpcServer().update_description(
1996 self.GetIssue(), self.description)
1997
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001999 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002001 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002002 return self.SetFlags({flag: value})
2003
2004 def SetFlags(self, flags):
2005 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002006 """
phajdan.jr68598232016-08-10 03:28:28 -07002007 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002008 try:
tandrii4b233bd2016-07-06 03:50:29 -07002009 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002010 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002011 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002012 if e.code == 404:
2013 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2014 if e.code == 403:
2015 DieWithError(
2016 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002017 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002018 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002020 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021 """Returns an upload.RpcServer() to access this review's rietveld instance.
2022 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002023 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002024 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002025 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002026 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002027 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002029 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002030 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002031 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
tandrii5d48c322016-08-18 16:19:37 -07002033 @classmethod
2034 def PatchsetConfigKey(cls):
2035 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036
tandrii5d48c322016-08-18 16:19:37 -07002037 @classmethod
2038 def CodereviewServerConfigKey(cls):
2039 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002040
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002041 def GetRieveldObjForPresubmit(self):
2042 return self.RpcServer()
2043
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002044 def SetCQState(self, new_state):
2045 props = self.GetIssueProperties()
2046 if props.get('private'):
2047 DieWithError('Cannot set-commit on private issue')
2048
2049 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002050 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002051 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002052 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002053 else:
tandrii4b233bd2016-07-06 03:50:29 -07002054 assert new_state == _CQState.DRY_RUN
2055 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002056
2057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002058 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2059 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 # PatchIssue should never be called with a dirty tree. It is up to the
2061 # caller to check this, but just in case we assert here since the
2062 # consequences of the caller not checking this could be dire.
2063 assert(not git_common.is_dirty_git_tree('apply'))
2064 assert(parsed_issue_arg.valid)
2065 self._changelist.issue = parsed_issue_arg.issue
2066 if parsed_issue_arg.hostname:
2067 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2068
skobes6468b902016-10-24 08:45:10 -07002069 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2070 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2071 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002072 try:
skobes6468b902016-10-24 08:45:10 -07002073 scm_obj.apply_patch(patchset_object)
2074 except Exception as e:
2075 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002076 return 1
2077
2078 # If we had an issue, commit the current state and register the issue.
2079 if not nocommit:
2080 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2081 'patch from issue %(i)s at patchset '
2082 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2083 % {'i': self.GetIssue(), 'p': patchset})])
2084 self.SetIssue(self.GetIssue())
2085 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002086 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002087 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002088 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002089 return 0
2090
2091 @staticmethod
2092 def ParseIssueURL(parsed_url):
2093 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2094 return None
wychen3c1c1722016-08-04 11:46:36 -07002095 # Rietveld patch: https://domain/<number>/#ps<patchset>
2096 match = re.match(r'/(\d+)/$', parsed_url.path)
2097 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2098 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002099 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002100 issue=int(match.group(1)),
2101 patchset=int(match2.group(1)),
2102 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002103 # Typical url: https://domain/<issue_number>[/[other]]
2104 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2105 if match:
skobes6468b902016-10-24 08:45:10 -07002106 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 issue=int(match.group(1)),
2108 hostname=parsed_url.netloc)
2109 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2110 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2111 if match:
skobes6468b902016-10-24 08:45:10 -07002112 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002113 issue=int(match.group(1)),
2114 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002115 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 return None
2117
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002118 def CMDUploadChange(self, options, args, change):
2119 """Upload the patch to Rietveld."""
2120 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2121 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2123 if options.emulate_svn_auto_props:
2124 upload_args.append('--emulate_svn_auto_props')
2125
2126 change_desc = None
2127
2128 if options.email is not None:
2129 upload_args.extend(['--email', options.email])
2130
2131 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002132 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 upload_args.extend(['--title', options.title])
2134 if options.message:
2135 upload_args.extend(['--message', options.message])
2136 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002137 print('This branch is associated with issue %s. '
2138 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002139 else:
nodirca166002016-06-27 10:59:51 -07002140 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002141 upload_args.extend(['--title', options.title])
2142 message = (options.title or options.message or
2143 CreateDescriptionFromLog(args))
2144 change_desc = ChangeDescription(message)
2145 if options.reviewers or options.tbr_owners:
2146 change_desc.update_reviewers(options.reviewers,
2147 options.tbr_owners,
2148 change)
2149 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002150 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151
2152 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002153 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002154 return 1
2155
2156 upload_args.extend(['--message', change_desc.description])
2157 if change_desc.get_reviewers():
2158 upload_args.append('--reviewers=%s' % ','.join(
2159 change_desc.get_reviewers()))
2160 if options.send_mail:
2161 if not change_desc.get_reviewers():
2162 DieWithError("Must specify reviewers to send email.")
2163 upload_args.append('--send_mail')
2164
2165 # We check this before applying rietveld.private assuming that in
2166 # rietveld.cc only addresses which we can send private CLs to are listed
2167 # if rietveld.private is set, and so we should ignore rietveld.cc only
2168 # when --private is specified explicitly on the command line.
2169 if options.private:
2170 logging.warn('rietveld.cc is ignored since private flag is specified. '
2171 'You need to review and add them manually if necessary.')
2172 cc = self.GetCCListWithoutDefault()
2173 else:
2174 cc = self.GetCCList()
2175 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002176 if change_desc.get_cced():
2177 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 if cc:
2179 upload_args.extend(['--cc', cc])
2180
2181 if options.private or settings.GetDefaultPrivateFlag() == "True":
2182 upload_args.append('--private')
2183
2184 upload_args.extend(['--git_similarity', str(options.similarity)])
2185 if not options.find_copies:
2186 upload_args.extend(['--git_no_find_copies'])
2187
2188 # Include the upstream repo's URL in the change -- this is useful for
2189 # projects that have their source spread across multiple repos.
2190 remote_url = self.GetGitBaseUrlFromConfig()
2191 if not remote_url:
2192 if settings.GetIsGitSvn():
2193 remote_url = self.GetGitSvnRemoteUrl()
2194 else:
2195 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2196 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2197 self.GetUpstreamBranch().split('/')[-1])
2198 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199 remote, remote_branch = self.GetRemoteBranch()
2200 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2201 settings.GetPendingRefPrefix())
2202 if target_ref:
2203 upload_args.extend(['--target_ref', target_ref])
2204
2205 # Look for dependent patchsets. See crbug.com/480453 for more details.
2206 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2207 upstream_branch = ShortBranchName(upstream_branch)
2208 if remote is '.':
2209 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002210 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002212 print()
2213 print('Skipping dependency patchset upload because git config '
2214 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2215 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 else:
2217 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002218 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 auth_config=auth_config)
2220 branch_cl_issue_url = branch_cl.GetIssueURL()
2221 branch_cl_issue = branch_cl.GetIssue()
2222 branch_cl_patchset = branch_cl.GetPatchset()
2223 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2224 upload_args.extend(
2225 ['--depends_on_patchset', '%s:%s' % (
2226 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002227 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 '\n'
2229 'The current branch (%s) is tracking a local branch (%s) with '
2230 'an associated CL.\n'
2231 'Adding %s/#ps%s as a dependency patchset.\n'
2232 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2233 branch_cl_patchset))
2234
2235 project = settings.GetProject()
2236 if project:
2237 upload_args.extend(['--project', project])
2238
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 try:
2240 upload_args = ['upload'] + upload_args + args
2241 logging.info('upload.RealMain(%s)', upload_args)
2242 issue, patchset = upload.RealMain(upload_args)
2243 issue = int(issue)
2244 patchset = int(patchset)
2245 except KeyboardInterrupt:
2246 sys.exit(1)
2247 except:
2248 # If we got an exception after the user typed a description for their
2249 # change, back up the description before re-raising.
2250 if change_desc:
2251 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2252 print('\nGot exception while uploading -- saving description to %s\n' %
2253 backup_path)
2254 backup_file = open(backup_path, 'w')
2255 backup_file.write(change_desc.description)
2256 backup_file.close()
2257 raise
2258
2259 if not self.GetIssue():
2260 self.SetIssue(issue)
2261 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002262 return 0
2263
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002264
2265class _GerritChangelistImpl(_ChangelistCodereviewBase):
2266 def __init__(self, changelist, auth_config=None):
2267 # auth_config is Rietveld thing, kept here to preserve interface only.
2268 super(_GerritChangelistImpl, self).__init__(changelist)
2269 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002270 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002271 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002272 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002273
2274 def _GetGerritHost(self):
2275 # Lazy load of configs.
2276 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002277 if self._gerrit_host and '.' not in self._gerrit_host:
2278 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2279 # This happens for internal stuff http://crbug.com/614312.
2280 parsed = urlparse.urlparse(self.GetRemoteUrl())
2281 if parsed.scheme == 'sso':
2282 print('WARNING: using non https URLs for remote is likely broken\n'
2283 ' Your current remote is: %s' % self.GetRemoteUrl())
2284 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2285 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002286 return self._gerrit_host
2287
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002288 def _GetGitHost(self):
2289 """Returns git host to be used when uploading change to Gerrit."""
2290 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2291
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002292 def GetCodereviewServer(self):
2293 if not self._gerrit_server:
2294 # If we're on a branch then get the server potentially associated
2295 # with that branch.
2296 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002297 self._gerrit_server = self._GitGetBranchConfigValue(
2298 self.CodereviewServerConfigKey())
2299 if self._gerrit_server:
2300 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002301 if not self._gerrit_server:
2302 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2303 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002304 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002305 parts[0] = parts[0] + '-review'
2306 self._gerrit_host = '.'.join(parts)
2307 self._gerrit_server = 'https://%s' % self._gerrit_host
2308 return self._gerrit_server
2309
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002310 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002311 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002312 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002313
tandrii5d48c322016-08-18 16:19:37 -07002314 @classmethod
2315 def PatchsetConfigKey(cls):
2316 return 'gerritpatchset'
2317
2318 @classmethod
2319 def CodereviewServerConfigKey(cls):
2320 return 'gerritserver'
2321
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002322 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002323 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002324 if settings.GetGerritSkipEnsureAuthenticated():
2325 # For projects with unusual authentication schemes.
2326 # See http://crbug.com/603378.
2327 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002328 # Lazy-loader to identify Gerrit and Git hosts.
2329 if gerrit_util.GceAuthenticator.is_gce():
2330 return
2331 self.GetCodereviewServer()
2332 git_host = self._GetGitHost()
2333 assert self._gerrit_server and self._gerrit_host
2334 cookie_auth = gerrit_util.CookiesAuthenticator()
2335
2336 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2337 git_auth = cookie_auth.get_auth_header(git_host)
2338 if gerrit_auth and git_auth:
2339 if gerrit_auth == git_auth:
2340 return
2341 print((
2342 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2343 ' Check your %s or %s file for credentials of hosts:\n'
2344 ' %s\n'
2345 ' %s\n'
2346 ' %s') %
2347 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2348 git_host, self._gerrit_host,
2349 cookie_auth.get_new_password_message(git_host)))
2350 if not force:
2351 ask_for_data('If you know what you are doing, press Enter to continue, '
2352 'Ctrl+C to abort.')
2353 return
2354 else:
2355 missing = (
2356 [] if gerrit_auth else [self._gerrit_host] +
2357 [] if git_auth else [git_host])
2358 DieWithError('Credentials for the following hosts are required:\n'
2359 ' %s\n'
2360 'These are read from %s (or legacy %s)\n'
2361 '%s' % (
2362 '\n '.join(missing),
2363 cookie_auth.get_gitcookies_path(),
2364 cookie_auth.get_netrc_path(),
2365 cookie_auth.get_new_password_message(git_host)))
2366
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002367 def _PostUnsetIssueProperties(self):
2368 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002369 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002370
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002371 def GetRieveldObjForPresubmit(self):
2372 class ThisIsNotRietveldIssue(object):
2373 def __nonzero__(self):
2374 # This is a hack to make presubmit_support think that rietveld is not
2375 # defined, yet still ensure that calls directly result in a decent
2376 # exception message below.
2377 return False
2378
2379 def __getattr__(self, attr):
2380 print(
2381 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2382 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2383 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2384 'or use Rietveld for codereview.\n'
2385 'See also http://crbug.com/579160.' % attr)
2386 raise NotImplementedError()
2387 return ThisIsNotRietveldIssue()
2388
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002389 def GetGerritObjForPresubmit(self):
2390 return presubmit_support.GerritAccessor(self._GetGerritHost())
2391
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002393 """Apply a rough heuristic to give a simple summary of an issue's review
2394 or CQ status, assuming adherence to a common workflow.
2395
2396 Returns None if no issue for this branch, or one of the following keywords:
2397 * 'error' - error from review tool (including deleted issues)
2398 * 'unsent' - no reviewers added
2399 * 'waiting' - waiting for review
2400 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002401 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2402 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002403 * 'commit' - in the commit queue
2404 * 'closed' - abandoned
2405 """
2406 if not self.GetIssue():
2407 return None
2408
2409 try:
2410 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002411 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412 return 'error'
2413
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002414 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002415 return 'closed'
2416
2417 cq_label = data['labels'].get('Commit-Queue', {})
2418 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002419 votes = cq_label.get('all', [])
2420 highest_vote = 0
2421 for v in votes:
2422 highest_vote = max(highest_vote, v.get('value', 0))
2423 vote_value = str(highest_vote)
2424 if vote_value != '0':
2425 # Add a '+' if the value is not 0 to match the values in the label.
2426 # The cq_label does not have negatives.
2427 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002428 vote_text = cq_label.get('values', {}).get(vote_value, '')
2429 if vote_text.lower() == 'commit':
2430 return 'commit'
2431
2432 lgtm_label = data['labels'].get('Code-Review', {})
2433 if lgtm_label:
2434 if 'rejected' in lgtm_label:
2435 return 'not lgtm'
2436 if 'approved' in lgtm_label:
2437 return 'lgtm'
2438
2439 if not data.get('reviewers', {}).get('REVIEWER', []):
2440 return 'unsent'
2441
2442 messages = data.get('messages', [])
2443 if messages:
2444 owner = data['owner'].get('_account_id')
2445 last_message_author = messages[-1].get('author', {}).get('_account_id')
2446 if owner != last_message_author:
2447 # Some reply from non-owner.
2448 return 'reply'
2449
2450 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002451
2452 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002453 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002454 return data['revisions'][data['current_revision']]['_number']
2455
2456 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002457 data = self._GetChangeDetail(['CURRENT_REVISION'])
2458 current_rev = data['current_revision']
2459 url = data['revisions'][current_rev]['fetch']['http']['url']
2460 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002461
dsansomee2d6fd92016-09-08 00:10:47 -07002462 def UpdateDescriptionRemote(self, description, force=False):
2463 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2464 if not force:
2465 ask_for_data(
2466 'The description cannot be modified while the issue has a pending '
2467 'unpublished edit. Either publish the edit in the Gerrit web UI '
2468 'or delete it.\n\n'
2469 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2470
2471 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2472 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002473 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2474 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002475
2476 def CloseIssue(self):
2477 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2478
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002479 def GetApprovingReviewers(self):
2480 """Returns a list of reviewers approving the change.
2481
2482 Note: not necessarily committers.
2483 """
2484 raise NotImplementedError()
2485
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002486 def SubmitIssue(self, wait_for_merge=True):
2487 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2488 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002490 def _GetChangeDetail(self, options=None, issue=None):
2491 options = options or []
2492 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002493 assert issue, 'issue is required to query Gerrit'
2494 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002495 options)
tandriic2405f52016-10-10 08:13:15 -07002496 if not data:
2497 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2498 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002499
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002500 def CMDLand(self, force, bypass_hooks, verbose):
2501 if git_common.is_dirty_git_tree('land'):
2502 return 1
tandriid60367b2016-06-22 05:25:12 -07002503 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2504 if u'Commit-Queue' in detail.get('labels', {}):
2505 if not force:
2506 ask_for_data('\nIt seems this repository has a Commit Queue, '
2507 'which can test and land changes for you. '
2508 'Are you sure you wish to bypass it?\n'
2509 'Press Enter to continue, Ctrl+C to abort.')
2510
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002511 differs = True
tandriic4344b52016-08-29 06:04:54 -07002512 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002513 # Note: git diff outputs nothing if there is no diff.
2514 if not last_upload or RunGit(['diff', last_upload]).strip():
2515 print('WARNING: some changes from local branch haven\'t been uploaded')
2516 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002517 if detail['current_revision'] == last_upload:
2518 differs = False
2519 else:
2520 print('WARNING: local branch contents differ from latest uploaded '
2521 'patchset')
2522 if differs:
2523 if not force:
2524 ask_for_data(
2525 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2526 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2527 elif not bypass_hooks:
2528 hook_results = self.RunHook(
2529 committing=True,
2530 may_prompt=not force,
2531 verbose=verbose,
2532 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2533 if not hook_results.should_continue():
2534 return 1
2535
2536 self.SubmitIssue(wait_for_merge=True)
2537 print('Issue %s has been submitted.' % self.GetIssueURL())
2538 return 0
2539
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002540 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2541 directory):
2542 assert not reject
2543 assert not nocommit
2544 assert not directory
2545 assert parsed_issue_arg.valid
2546
2547 self._changelist.issue = parsed_issue_arg.issue
2548
2549 if parsed_issue_arg.hostname:
2550 self._gerrit_host = parsed_issue_arg.hostname
2551 self._gerrit_server = 'https://%s' % self._gerrit_host
2552
tandriic2405f52016-10-10 08:13:15 -07002553 try:
2554 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2555 except GerritIssueNotExists as e:
2556 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002557
2558 if not parsed_issue_arg.patchset:
2559 # Use current revision by default.
2560 revision_info = detail['revisions'][detail['current_revision']]
2561 patchset = int(revision_info['_number'])
2562 else:
2563 patchset = parsed_issue_arg.patchset
2564 for revision_info in detail['revisions'].itervalues():
2565 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2566 break
2567 else:
2568 DieWithError('Couldn\'t find patchset %i in issue %i' %
2569 (parsed_issue_arg.patchset, self.GetIssue()))
2570
2571 fetch_info = revision_info['fetch']['http']
2572 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2573 RunGit(['cherry-pick', 'FETCH_HEAD'])
2574 self.SetIssue(self.GetIssue())
2575 self.SetPatchset(patchset)
2576 print('Committed patch for issue %i pathset %i locally' %
2577 (self.GetIssue(), self.GetPatchset()))
2578 return 0
2579
2580 @staticmethod
2581 def ParseIssueURL(parsed_url):
2582 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2583 return None
2584 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2585 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2586 # Short urls like https://domain/<issue_number> can be used, but don't allow
2587 # specifying the patchset (you'd 404), but we allow that here.
2588 if parsed_url.path == '/':
2589 part = parsed_url.fragment
2590 else:
2591 part = parsed_url.path
2592 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2593 if match:
2594 return _ParsedIssueNumberArgument(
2595 issue=int(match.group(2)),
2596 patchset=int(match.group(4)) if match.group(4) else None,
2597 hostname=parsed_url.netloc)
2598 return None
2599
tandrii16e0b4e2016-06-07 10:34:28 -07002600 def _GerritCommitMsgHookCheck(self, offer_removal):
2601 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2602 if not os.path.exists(hook):
2603 return
2604 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2605 # custom developer made one.
2606 data = gclient_utils.FileRead(hook)
2607 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2608 return
2609 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002610 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002611 'and may interfere with it in subtle ways.\n'
2612 'We recommend you remove the commit-msg hook.')
2613 if offer_removal:
2614 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2615 if reply.lower().startswith('y'):
2616 gclient_utils.rm_file_or_tree(hook)
2617 print('Gerrit commit-msg hook removed.')
2618 else:
2619 print('OK, will keep Gerrit commit-msg hook in place.')
2620
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002621 def CMDUploadChange(self, options, args, change):
2622 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002623 if options.squash and options.no_squash:
2624 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002625
2626 if not options.squash and not options.no_squash:
2627 # Load default for user, repo, squash=true, in this order.
2628 options.squash = settings.GetSquashGerritUploads()
2629 elif options.no_squash:
2630 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002631
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002632 # We assume the remote called "origin" is the one we want.
2633 # It is probably not worthwhile to support different workflows.
2634 gerrit_remote = 'origin'
2635
2636 remote, remote_branch = self.GetRemoteBranch()
2637 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2638 pending_prefix='')
2639
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002641 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 if self.GetIssue():
2643 # Try to get the message from a previous upload.
2644 message = self.GetDescription()
2645 if not message:
2646 DieWithError(
2647 'failed to fetch description from current Gerrit issue %d\n'
2648 '%s' % (self.GetIssue(), self.GetIssueURL()))
2649 change_id = self._GetChangeDetail()['change_id']
2650 while True:
2651 footer_change_ids = git_footers.get_footer_change_id(message)
2652 if footer_change_ids == [change_id]:
2653 break
2654 if not footer_change_ids:
2655 message = git_footers.add_footer_change_id(message, change_id)
2656 print('WARNING: appended missing Change-Id to issue description')
2657 continue
2658 # There is already a valid footer but with different or several ids.
2659 # Doing this automatically is non-trivial as we don't want to lose
2660 # existing other footers, yet we want to append just 1 desired
2661 # Change-Id. Thus, just create a new footer, but let user verify the
2662 # new description.
2663 message = '%s\n\nChange-Id: %s' % (message, change_id)
2664 print(
2665 'WARNING: issue %s has Change-Id footer(s):\n'
2666 ' %s\n'
2667 'but issue has Change-Id %s, according to Gerrit.\n'
2668 'Please, check the proposed correction to the description, '
2669 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2670 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2671 change_id))
2672 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2673 if not options.force:
2674 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002675 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002676 message = change_desc.description
2677 if not message:
2678 DieWithError("Description is empty. Aborting...")
2679 # Continue the while loop.
2680 # Sanity check of this code - we should end up with proper message
2681 # footer.
2682 assert [change_id] == git_footers.get_footer_change_id(message)
2683 change_desc = ChangeDescription(message)
2684 else:
2685 change_desc = ChangeDescription(
2686 options.message or CreateDescriptionFromLog(args))
2687 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002688 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 if not change_desc.description:
2690 DieWithError("Description is empty. Aborting...")
2691 message = change_desc.description
2692 change_ids = git_footers.get_footer_change_id(message)
2693 if len(change_ids) > 1:
2694 DieWithError('too many Change-Id footers, at most 1 allowed.')
2695 if not change_ids:
2696 # Generate the Change-Id automatically.
2697 message = git_footers.add_footer_change_id(
2698 message, GenerateGerritChangeId(message))
2699 change_desc.set_description(message)
2700 change_ids = git_footers.get_footer_change_id(message)
2701 assert len(change_ids) == 1
2702 change_id = change_ids[0]
2703
2704 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2705 if remote is '.':
2706 # If our upstream branch is local, we base our squashed commit on its
2707 # squashed version.
2708 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2709 # Check the squashed hash of the parent.
2710 parent = RunGit(['config',
2711 'branch.%s.gerritsquashhash' % upstream_branch_name],
2712 error_ok=True).strip()
2713 # Verify that the upstream branch has been uploaded too, otherwise
2714 # Gerrit will create additional CLs when uploading.
2715 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2716 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002717 DieWithError(
2718 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002719 'Note: maybe you\'ve uploaded it with --no-squash. '
2720 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 ' git cl upload --squash\n' % upstream_branch_name)
2722 else:
2723 parent = self.GetCommonAncestorWithUpstream()
2724
2725 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2726 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2727 '-m', message]).strip()
2728 else:
2729 change_desc = ChangeDescription(
2730 options.message or CreateDescriptionFromLog(args))
2731 if not change_desc.description:
2732 DieWithError("Description is empty. Aborting...")
2733
2734 if not git_footers.get_footer_change_id(change_desc.description):
2735 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002736 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2737 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738 ref_to_push = 'HEAD'
2739 parent = '%s/%s' % (gerrit_remote, branch)
2740 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2741
2742 assert change_desc
2743 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2744 ref_to_push)]).splitlines()
2745 if len(commits) > 1:
2746 print('WARNING: This will upload %d commits. Run the following command '
2747 'to see which commits will be uploaded: ' % len(commits))
2748 print('git log %s..%s' % (parent, ref_to_push))
2749 print('You can also use `git squash-branch` to squash these into a '
2750 'single commit.')
2751 ask_for_data('About to upload; enter to confirm.')
2752
2753 if options.reviewers or options.tbr_owners:
2754 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2755 change)
2756
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002757 # Extra options that can be specified at push time. Doc:
2758 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2759 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002760 if change_desc.get_reviewers(tbr_only=True):
2761 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2762 refspec_opts.append('l=Code-Review+1')
2763
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002764 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002765 if not re.match(r'^[\w ]+$', options.title):
2766 options.title = re.sub(r'[^\w ]', '', options.title)
2767 print('WARNING: Patchset title may only contain alphanumeric chars '
2768 'and spaces. Cleaned up title:\n%s' % options.title)
2769 if not options.force:
2770 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002771 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2772 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002773 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2774
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002775 if options.send_mail:
2776 if not change_desc.get_reviewers():
2777 DieWithError('Must specify reviewers to send email.')
2778 refspec_opts.append('notify=ALL')
2779 else:
2780 refspec_opts.append('notify=NONE')
2781
tandrii99a72f22016-08-17 14:33:24 -07002782 reviewers = change_desc.get_reviewers()
2783 if reviewers:
2784 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002785
agablec6787972016-09-09 16:13:34 -07002786 if options.private:
2787 refspec_opts.append('draft')
2788
rmistry9eadede2016-09-19 11:22:43 -07002789 if options.topic:
2790 # Documentation on Gerrit topics is here:
2791 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2792 refspec_opts.append('topic=%s' % options.topic)
2793
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002794 refspec_suffix = ''
2795 if refspec_opts:
2796 refspec_suffix = '%' + ','.join(refspec_opts)
2797 assert ' ' not in refspec_suffix, (
2798 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002799 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002800
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002801 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002802 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803 print_stdout=True,
2804 # Flush after every line: useful for seeing progress when running as
2805 # recipe.
2806 filter_fn=lambda _: sys.stdout.flush())
2807
2808 if options.squash:
2809 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2810 change_numbers = [m.group(1)
2811 for m in map(regex.match, push_stdout.splitlines())
2812 if m]
2813 if len(change_numbers) != 1:
2814 DieWithError(
2815 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2816 'Change-Id: %s') % (len(change_numbers), change_id))
2817 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002818 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002819
2820 # Add cc's from the CC_LIST and --cc flag (if any).
2821 cc = self.GetCCList().split(',')
2822 if options.cc:
2823 cc.extend(options.cc)
2824 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002825 if change_desc.get_cced():
2826 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002827 if cc:
2828 gerrit_util.AddReviewers(
2829 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2830
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002831 return 0
2832
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002833 def _AddChangeIdToCommitMessage(self, options, args):
2834 """Re-commits using the current message, assumes the commit hook is in
2835 place.
2836 """
2837 log_desc = options.message or CreateDescriptionFromLog(args)
2838 git_command = ['commit', '--amend', '-m', log_desc]
2839 RunGit(git_command)
2840 new_log_desc = CreateDescriptionFromLog(args)
2841 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002842 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002843 return new_log_desc
2844 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002845 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002846
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002847 def SetCQState(self, new_state):
2848 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002849 vote_map = {
2850 _CQState.NONE: 0,
2851 _CQState.DRY_RUN: 1,
2852 _CQState.COMMIT : 2,
2853 }
2854 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2855 labels={'Commit-Queue': vote_map[new_state]})
2856
tandriie113dfd2016-10-11 10:20:12 -07002857 def CannotTriggerTryJobReason(self):
2858 # TODO(tandrii): implement for Gerrit.
2859 raise NotImplementedError()
2860
tandriide281ae2016-10-12 06:02:30 -07002861 def GetIssueOwner(self):
2862 # TODO(tandrii): implement for Gerrit.
2863 raise NotImplementedError()
2864
2865 def GetIssueProject(self):
2866 # TODO(tandrii): implement for Gerrit.
2867 raise NotImplementedError()
2868
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002869
2870_CODEREVIEW_IMPLEMENTATIONS = {
2871 'rietveld': _RietveldChangelistImpl,
2872 'gerrit': _GerritChangelistImpl,
2873}
2874
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002875
iannuccie53c9352016-08-17 14:40:40 -07002876def _add_codereview_issue_select_options(parser, extra=""):
2877 _add_codereview_select_options(parser)
2878
2879 text = ('Operate on this issue number instead of the current branch\'s '
2880 'implicit issue.')
2881 if extra:
2882 text += ' '+extra
2883 parser.add_option('-i', '--issue', type=int, help=text)
2884
2885
2886def _process_codereview_issue_select_options(parser, options):
2887 _process_codereview_select_options(parser, options)
2888 if options.issue is not None and not options.forced_codereview:
2889 parser.error('--issue must be specified with either --rietveld or --gerrit')
2890
2891
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002892def _add_codereview_select_options(parser):
2893 """Appends --gerrit and --rietveld options to force specific codereview."""
2894 parser.codereview_group = optparse.OptionGroup(
2895 parser, 'EXPERIMENTAL! Codereview override options')
2896 parser.add_option_group(parser.codereview_group)
2897 parser.codereview_group.add_option(
2898 '--gerrit', action='store_true',
2899 help='Force the use of Gerrit for codereview')
2900 parser.codereview_group.add_option(
2901 '--rietveld', action='store_true',
2902 help='Force the use of Rietveld for codereview')
2903
2904
2905def _process_codereview_select_options(parser, options):
2906 if options.gerrit and options.rietveld:
2907 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2908 options.forced_codereview = None
2909 if options.gerrit:
2910 options.forced_codereview = 'gerrit'
2911 elif options.rietveld:
2912 options.forced_codereview = 'rietveld'
2913
2914
tandriif9aefb72016-07-01 09:06:51 -07002915def _get_bug_line_values(default_project, bugs):
2916 """Given default_project and comma separated list of bugs, yields bug line
2917 values.
2918
2919 Each bug can be either:
2920 * a number, which is combined with default_project
2921 * string, which is left as is.
2922
2923 This function may produce more than one line, because bugdroid expects one
2924 project per line.
2925
2926 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2927 ['v8:123', 'chromium:789']
2928 """
2929 default_bugs = []
2930 others = []
2931 for bug in bugs.split(','):
2932 bug = bug.strip()
2933 if bug:
2934 try:
2935 default_bugs.append(int(bug))
2936 except ValueError:
2937 others.append(bug)
2938
2939 if default_bugs:
2940 default_bugs = ','.join(map(str, default_bugs))
2941 if default_project:
2942 yield '%s:%s' % (default_project, default_bugs)
2943 else:
2944 yield default_bugs
2945 for other in sorted(others):
2946 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2947 yield other
2948
2949
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002950class ChangeDescription(object):
2951 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002952 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002953 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002954 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002955
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002956 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002957 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002958
agable@chromium.org42c20792013-09-12 17:34:49 +00002959 @property # www.logilab.org/ticket/89786
2960 def description(self): # pylint: disable=E0202
2961 return '\n'.join(self._description_lines)
2962
2963 def set_description(self, desc):
2964 if isinstance(desc, basestring):
2965 lines = desc.splitlines()
2966 else:
2967 lines = [line.rstrip() for line in desc]
2968 while lines and not lines[0]:
2969 lines.pop(0)
2970 while lines and not lines[-1]:
2971 lines.pop(-1)
2972 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002973
piman@chromium.org336f9122014-09-04 02:16:55 +00002974 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002975 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002976 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002977 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002978 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002979 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002980
agable@chromium.org42c20792013-09-12 17:34:49 +00002981 # Get the set of R= and TBR= lines and remove them from the desciption.
2982 regexp = re.compile(self.R_LINE)
2983 matches = [regexp.match(line) for line in self._description_lines]
2984 new_desc = [l for i, l in enumerate(self._description_lines)
2985 if not matches[i]]
2986 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002987
agable@chromium.org42c20792013-09-12 17:34:49 +00002988 # Construct new unified R= and TBR= lines.
2989 r_names = []
2990 tbr_names = []
2991 for match in matches:
2992 if not match:
2993 continue
2994 people = cleanup_list([match.group(2).strip()])
2995 if match.group(1) == 'TBR':
2996 tbr_names.extend(people)
2997 else:
2998 r_names.extend(people)
2999 for name in r_names:
3000 if name not in reviewers:
3001 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003002 if add_owners_tbr:
3003 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003004 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003005 all_reviewers = set(tbr_names + reviewers)
3006 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3007 all_reviewers)
3008 tbr_names.extend(owners_db.reviewers_for(missing_files,
3009 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003010 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3011 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3012
3013 # Put the new lines in the description where the old first R= line was.
3014 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3015 if 0 <= line_loc < len(self._description_lines):
3016 if new_tbr_line:
3017 self._description_lines.insert(line_loc, new_tbr_line)
3018 if new_r_line:
3019 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003021 if new_r_line:
3022 self.append_footer(new_r_line)
3023 if new_tbr_line:
3024 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025
tandriif9aefb72016-07-01 09:06:51 -07003026 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 self.set_description([
3029 '# Enter a description of the change.',
3030 '# This will be displayed on the codereview site.',
3031 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003032 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003033 '--------------------',
3034 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003035
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 regexp = re.compile(self.BUG_LINE)
3037 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003038 prefix = settings.GetBugPrefix()
3039 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3040 for value in values:
3041 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3042 self.append_footer('BUG=%s' % value)
3043
agable@chromium.org42c20792013-09-12 17:34:49 +00003044 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003045 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003046 if not content:
3047 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003048 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003049
3050 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003051 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3052 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003053 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003054 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003055
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003056 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003057 """Adds a footer line to the description.
3058
3059 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3060 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3061 that Gerrit footers are always at the end.
3062 """
3063 parsed_footer_line = git_footers.parse_footer(line)
3064 if parsed_footer_line:
3065 # Line is a gerrit footer in the form: Footer-Key: any value.
3066 # Thus, must be appended observing Gerrit footer rules.
3067 self.set_description(
3068 git_footers.add_footer(self.description,
3069 key=parsed_footer_line[0],
3070 value=parsed_footer_line[1]))
3071 return
3072
3073 if not self._description_lines:
3074 self._description_lines.append(line)
3075 return
3076
3077 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3078 if gerrit_footers:
3079 # git_footers.split_footers ensures that there is an empty line before
3080 # actual (gerrit) footers, if any. We have to keep it that way.
3081 assert top_lines and top_lines[-1] == ''
3082 top_lines, separator = top_lines[:-1], top_lines[-1:]
3083 else:
3084 separator = [] # No need for separator if there are no gerrit_footers.
3085
3086 prev_line = top_lines[-1] if top_lines else ''
3087 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3088 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3089 top_lines.append('')
3090 top_lines.append(line)
3091 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003092
tandrii99a72f22016-08-17 14:33:24 -07003093 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003094 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003095 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003096 reviewers = [match.group(2).strip()
3097 for match in matches
3098 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003099 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003100
bradnelsond975b302016-10-23 12:20:23 -07003101 def get_cced(self):
3102 """Retrieves the list of reviewers."""
3103 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3104 cced = [match.group(2).strip() for match in matches if match]
3105 return cleanup_list(cced)
3106
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003107
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003108def get_approving_reviewers(props):
3109 """Retrieves the reviewers that approved a CL from the issue properties with
3110 messages.
3111
3112 Note that the list may contain reviewers that are not committer, thus are not
3113 considered by the CQ.
3114 """
3115 return sorted(
3116 set(
3117 message['sender']
3118 for message in props['messages']
3119 if message['approval'] and message['sender'] in props['reviewers']
3120 )
3121 )
3122
3123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003124def FindCodereviewSettingsFile(filename='codereview.settings'):
3125 """Finds the given file starting in the cwd and going up.
3126
3127 Only looks up to the top of the repository unless an
3128 'inherit-review-settings-ok' file exists in the root of the repository.
3129 """
3130 inherit_ok_file = 'inherit-review-settings-ok'
3131 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003132 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003133 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3134 root = '/'
3135 while True:
3136 if filename in os.listdir(cwd):
3137 if os.path.isfile(os.path.join(cwd, filename)):
3138 return open(os.path.join(cwd, filename))
3139 if cwd == root:
3140 break
3141 cwd = os.path.dirname(cwd)
3142
3143
3144def LoadCodereviewSettingsFromFile(fileobj):
3145 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003146 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003148 def SetProperty(name, setting, unset_error_ok=False):
3149 fullname = 'rietveld.' + name
3150 if setting in keyvals:
3151 RunGit(['config', fullname, keyvals[setting]])
3152 else:
3153 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3154
tandrii48df5812016-10-17 03:55:37 -07003155 if not keyvals.get('GERRIT_HOST', False):
3156 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003157 # Only server setting is required. Other settings can be absent.
3158 # In that case, we ignore errors raised during option deletion attempt.
3159 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003160 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003161 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3162 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003163 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003164 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003165 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3166 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003167 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003168 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003169 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003170 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003171 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3172 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003174 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003175 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003176
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003177 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003178 RunGit(['config', 'gerrit.squash-uploads',
3179 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003180
tandrii@chromium.org28253532016-04-14 13:46:56 +00003181 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003182 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003183 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3186 #should be of the form
3187 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3188 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3189 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3190 keyvals['ORIGIN_URL_CONFIG']])
3191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003192
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003193def urlretrieve(source, destination):
3194 """urllib is broken for SSL connections via a proxy therefore we
3195 can't use urllib.urlretrieve()."""
3196 with open(destination, 'w') as f:
3197 f.write(urllib2.urlopen(source).read())
3198
3199
ukai@chromium.org712d6102013-11-27 00:52:58 +00003200def hasSheBang(fname):
3201 """Checks fname is a #! script."""
3202 with open(fname) as f:
3203 return f.read(2).startswith('#!')
3204
3205
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003206# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3207def DownloadHooks(*args, **kwargs):
3208 pass
3209
3210
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003211def DownloadGerritHook(force):
3212 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003213
3214 Args:
3215 force: True to update hooks. False to install hooks if not present.
3216 """
3217 if not settings.GetIsGerrit():
3218 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003219 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003220 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3221 if not os.access(dst, os.X_OK):
3222 if os.path.exists(dst):
3223 if not force:
3224 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003225 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003226 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003227 if not hasSheBang(dst):
3228 DieWithError('Not a script: %s\n'
3229 'You need to download from\n%s\n'
3230 'into .git/hooks/commit-msg and '
3231 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003232 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3233 except Exception:
3234 if os.path.exists(dst):
3235 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003236 DieWithError('\nFailed to download hooks.\n'
3237 'You need to download from\n%s\n'
3238 'into .git/hooks/commit-msg and '
3239 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003240
3241
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003242
3243def GetRietveldCodereviewSettingsInteractively():
3244 """Prompt the user for settings."""
3245 server = settings.GetDefaultServerUrl(error_ok=True)
3246 prompt = 'Rietveld server (host[:port])'
3247 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3248 newserver = ask_for_data(prompt + ':')
3249 if not server and not newserver:
3250 newserver = DEFAULT_SERVER
3251 if newserver:
3252 newserver = gclient_utils.UpgradeToHttps(newserver)
3253 if newserver != server:
3254 RunGit(['config', 'rietveld.server', newserver])
3255
3256 def SetProperty(initial, caption, name, is_url):
3257 prompt = caption
3258 if initial:
3259 prompt += ' ("x" to clear) [%s]' % initial
3260 new_val = ask_for_data(prompt + ':')
3261 if new_val == 'x':
3262 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3263 elif new_val:
3264 if is_url:
3265 new_val = gclient_utils.UpgradeToHttps(new_val)
3266 if new_val != initial:
3267 RunGit(['config', 'rietveld.' + name, new_val])
3268
3269 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3270 SetProperty(settings.GetDefaultPrivateFlag(),
3271 'Private flag (rietveld only)', 'private', False)
3272 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3273 'tree-status-url', False)
3274 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3275 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3276 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3277 'run-post-upload-hook', False)
3278
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003279@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003281 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003282
tandrii5d0a0422016-09-14 06:24:35 -07003283 print('WARNING: git cl config works for Rietveld only')
3284 # TODO(tandrii): remove this once we switch to Gerrit.
3285 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003286 parser.add_option('--activate-update', action='store_true',
3287 help='activate auto-updating [rietveld] section in '
3288 '.git/config')
3289 parser.add_option('--deactivate-update', action='store_true',
3290 help='deactivate auto-updating [rietveld] section in '
3291 '.git/config')
3292 options, args = parser.parse_args(args)
3293
3294 if options.deactivate_update:
3295 RunGit(['config', 'rietveld.autoupdate', 'false'])
3296 return
3297
3298 if options.activate_update:
3299 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3300 return
3301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003303 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 return 0
3305
3306 url = args[0]
3307 if not url.endswith('codereview.settings'):
3308 url = os.path.join(url, 'codereview.settings')
3309
3310 # Load code review settings and download hooks (if available).
3311 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3312 return 0
3313
3314
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003315def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003316 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003317 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3318 branch = ShortBranchName(branchref)
3319 _, args = parser.parse_args(args)
3320 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003321 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003322 return RunGit(['config', 'branch.%s.base-url' % branch],
3323 error_ok=False).strip()
3324 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003325 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003326 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3327 error_ok=False).strip()
3328
3329
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003330def color_for_status(status):
3331 """Maps a Changelist status to color, for CMDstatus and other tools."""
3332 return {
3333 'unsent': Fore.RED,
3334 'waiting': Fore.BLUE,
3335 'reply': Fore.YELLOW,
3336 'lgtm': Fore.GREEN,
3337 'commit': Fore.MAGENTA,
3338 'closed': Fore.CYAN,
3339 'error': Fore.WHITE,
3340 }.get(status, Fore.WHITE)
3341
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003342
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003343def get_cl_statuses(changes, fine_grained, max_processes=None):
3344 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003345
3346 If fine_grained is true, this will fetch CL statuses from the server.
3347 Otherwise, simply indicate if there's a matching url for the given branches.
3348
3349 If max_processes is specified, it is used as the maximum number of processes
3350 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3351 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003352
3353 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003354 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003355 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003356 upload.verbosity = 0
3357
3358 if fine_grained:
3359 # Process one branch synchronously to work through authentication, then
3360 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003361 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003362 def fetch(cl):
3363 try:
3364 return (cl, cl.GetStatus())
3365 except:
3366 # See http://crbug.com/629863.
3367 logging.exception('failed to fetch status for %s:', cl)
3368 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003369 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003370
tandriiea9514a2016-08-17 12:32:37 -07003371 changes_to_fetch = changes[1:]
3372 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003373 # Exit early if there was only one branch to fetch.
3374 return
3375
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003376 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003377 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003378 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003379 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003380
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003381 fetched_cls = set()
3382 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003383 while True:
3384 try:
3385 row = it.next(timeout=5)
3386 except multiprocessing.TimeoutError:
3387 break
3388
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003389 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003390 yield row
3391
3392 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003393 for cl in set(changes_to_fetch) - fetched_cls:
3394 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003395
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003396 else:
3397 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003398 for cl in changes:
3399 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003400
rmistry@google.com2dd99862015-06-22 12:22:18 +00003401
3402def upload_branch_deps(cl, args):
3403 """Uploads CLs of local branches that are dependents of the current branch.
3404
3405 If the local branch dependency tree looks like:
3406 test1 -> test2.1 -> test3.1
3407 -> test3.2
3408 -> test2.2 -> test3.3
3409
3410 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3411 run on the dependent branches in this order:
3412 test2.1, test3.1, test3.2, test2.2, test3.3
3413
3414 Note: This function does not rebase your local dependent branches. Use it when
3415 you make a change to the parent branch that will not conflict with its
3416 dependent branches, and you would like their dependencies updated in
3417 Rietveld.
3418 """
3419 if git_common.is_dirty_git_tree('upload-branch-deps'):
3420 return 1
3421
3422 root_branch = cl.GetBranch()
3423 if root_branch is None:
3424 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3425 'Get on a branch!')
3426 if not cl.GetIssue() or not cl.GetPatchset():
3427 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3428 'patchset dependencies without an uploaded CL.')
3429
3430 branches = RunGit(['for-each-ref',
3431 '--format=%(refname:short) %(upstream:short)',
3432 'refs/heads'])
3433 if not branches:
3434 print('No local branches found.')
3435 return 0
3436
3437 # Create a dictionary of all local branches to the branches that are dependent
3438 # on it.
3439 tracked_to_dependents = collections.defaultdict(list)
3440 for b in branches.splitlines():
3441 tokens = b.split()
3442 if len(tokens) == 2:
3443 branch_name, tracked = tokens
3444 tracked_to_dependents[tracked].append(branch_name)
3445
vapiera7fbd5a2016-06-16 09:17:49 -07003446 print()
3447 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003448 dependents = []
3449 def traverse_dependents_preorder(branch, padding=''):
3450 dependents_to_process = tracked_to_dependents.get(branch, [])
3451 padding += ' '
3452 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003453 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003454 dependents.append(dependent)
3455 traverse_dependents_preorder(dependent, padding)
3456 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003457 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003458
3459 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003460 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003461 return 0
3462
vapiera7fbd5a2016-06-16 09:17:49 -07003463 print('This command will checkout all dependent branches and run '
3464 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003465 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3466
andybons@chromium.org962f9462016-02-03 20:00:42 +00003467 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003468 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003469 args.extend(['-t', 'Updated patchset dependency'])
3470
rmistry@google.com2dd99862015-06-22 12:22:18 +00003471 # Record all dependents that failed to upload.
3472 failures = {}
3473 # Go through all dependents, checkout the branch and upload.
3474 try:
3475 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003476 print()
3477 print('--------------------------------------')
3478 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003479 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003480 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003481 try:
3482 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003483 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003484 failures[dependent_branch] = 1
3485 except: # pylint: disable=W0702
3486 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003487 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003488 finally:
3489 # Swap back to the original root branch.
3490 RunGit(['checkout', '-q', root_branch])
3491
vapiera7fbd5a2016-06-16 09:17:49 -07003492 print()
3493 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003494 for dependent_branch in dependents:
3495 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003496 print(' %s : %s' % (dependent_branch, upload_status))
3497 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003498
3499 return 0
3500
3501
kmarshall3bff56b2016-06-06 18:31:47 -07003502def CMDarchive(parser, args):
3503 """Archives and deletes branches associated with closed changelists."""
3504 parser.add_option(
3505 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003506 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003507 parser.add_option(
3508 '-f', '--force', action='store_true',
3509 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003510 parser.add_option(
3511 '-d', '--dry-run', action='store_true',
3512 help='Skip the branch tagging and removal steps.')
3513 parser.add_option(
3514 '-t', '--notags', action='store_true',
3515 help='Do not tag archived branches. '
3516 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003517
3518 auth.add_auth_options(parser)
3519 options, args = parser.parse_args(args)
3520 if args:
3521 parser.error('Unsupported args: %s' % ' '.join(args))
3522 auth_config = auth.extract_auth_config_from_options(options)
3523
3524 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3525 if not branches:
3526 return 0
3527
vapiera7fbd5a2016-06-16 09:17:49 -07003528 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003529 changes = [Changelist(branchref=b, auth_config=auth_config)
3530 for b in branches.splitlines()]
3531 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3532 statuses = get_cl_statuses(changes,
3533 fine_grained=True,
3534 max_processes=options.maxjobs)
3535 proposal = [(cl.GetBranch(),
3536 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3537 for cl, status in statuses
3538 if status == 'closed']
3539 proposal.sort()
3540
3541 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003542 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003543 return 0
3544
3545 current_branch = GetCurrentBranch()
3546
vapiera7fbd5a2016-06-16 09:17:49 -07003547 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003548 if options.notags:
3549 for next_item in proposal:
3550 print(' ' + next_item[0])
3551 else:
3552 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3553 for next_item in proposal:
3554 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003555
kmarshall9249e012016-08-23 12:02:16 -07003556 # Quit now on precondition failure or if instructed by the user, either
3557 # via an interactive prompt or by command line flags.
3558 if options.dry_run:
3559 print('\nNo changes were made (dry run).\n')
3560 return 0
3561 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003562 print('You are currently on a branch \'%s\' which is associated with a '
3563 'closed codereview issue, so archive cannot proceed. Please '
3564 'checkout another branch and run this command again.' %
3565 current_branch)
3566 return 1
kmarshall9249e012016-08-23 12:02:16 -07003567 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003568 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3569 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003570 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003571 return 1
3572
3573 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003574 if not options.notags:
3575 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003576 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003577
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003579
3580 return 0
3581
3582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003584 """Show status of changelists.
3585
3586 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003587 - Red not sent for review or broken
3588 - Blue waiting for review
3589 - Yellow waiting for you to reply to review
3590 - Green LGTM'ed
3591 - Magenta in the commit queue
3592 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003593
3594 Also see 'git cl comments'.
3595 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003596 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003597 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003598 parser.add_option('-f', '--fast', action='store_true',
3599 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003600 parser.add_option(
3601 '-j', '--maxjobs', action='store', type=int,
3602 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003603
3604 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003605 _add_codereview_issue_select_options(
3606 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003607 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003608 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003609 if args:
3610 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612
iannuccie53c9352016-08-17 14:40:40 -07003613 if options.issue is not None and not options.field:
3614 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003617 cl = Changelist(auth_config=auth_config, issue=options.issue,
3618 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003620 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003621 elif options.field == 'id':
3622 issueid = cl.GetIssue()
3623 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003624 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625 elif options.field == 'patch':
3626 patchset = cl.GetPatchset()
3627 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003628 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003629 elif options.field == 'status':
3630 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003631 elif options.field == 'url':
3632 url = cl.GetIssueURL()
3633 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003634 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003635 return 0
3636
3637 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3638 if not branches:
3639 print('No local branch found.')
3640 return 0
3641
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003642 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003643 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003644 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003645 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003646 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003647 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003648 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003649
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003650 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003651 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3652 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3653 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003654 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003655 c, status = output.next()
3656 branch_statuses[c.GetBranch()] = status
3657 status = branch_statuses.pop(branch)
3658 url = cl.GetIssueURL()
3659 if url and (not status or status == 'error'):
3660 # The issue probably doesn't exist anymore.
3661 url += ' (broken)'
3662
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003663 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003664 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003665 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003666 color = ''
3667 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003668 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003669 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003670 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003671 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003672
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003673 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print()
3675 print('Current branch:',)
3676 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003677 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003678 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003679 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003681 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003682 print('Issue description:')
3683 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684 return 0
3685
3686
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003687def colorize_CMDstatus_doc():
3688 """To be called once in main() to add colors to git cl status help."""
3689 colors = [i for i in dir(Fore) if i[0].isupper()]
3690
3691 def colorize_line(line):
3692 for color in colors:
3693 if color in line.upper():
3694 # Extract whitespaces first and the leading '-'.
3695 indent = len(line) - len(line.lstrip(' ')) + 1
3696 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3697 return line
3698
3699 lines = CMDstatus.__doc__.splitlines()
3700 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3701
3702
phajdan.jre328cf92016-08-22 04:12:17 -07003703def write_json(path, contents):
3704 with open(path, 'w') as f:
3705 json.dump(contents, f)
3706
3707
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003708@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003710 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711
3712 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003713 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003714 parser.add_option('-r', '--reverse', action='store_true',
3715 help='Lookup the branch(es) for the specified issues. If '
3716 'no issues are specified, all branches with mapped '
3717 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003718 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003719 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003720 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003721 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003722
dnj@chromium.org406c4402015-03-03 17:22:28 +00003723 if options.reverse:
3724 branches = RunGit(['for-each-ref', 'refs/heads',
3725 '--format=%(refname:short)']).splitlines()
3726
3727 # Reverse issue lookup.
3728 issue_branch_map = {}
3729 for branch in branches:
3730 cl = Changelist(branchref=branch)
3731 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3732 if not args:
3733 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003734 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003735 for issue in args:
3736 if not issue:
3737 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003738 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print('Branch for issue number %s: %s' % (
3740 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003741 if options.json:
3742 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003743 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003744 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003745 if len(args) > 0:
3746 try:
3747 issue = int(args[0])
3748 except ValueError:
3749 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003750 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003751 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003753 if options.json:
3754 write_json(options.json, {
3755 'issue': cl.GetIssue(),
3756 'issue_url': cl.GetIssueURL(),
3757 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758 return 0
3759
3760
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003761def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003762 """Shows or posts review comments for any changelist."""
3763 parser.add_option('-a', '--add-comment', dest='comment',
3764 help='comment to add to an issue')
3765 parser.add_option('-i', dest='issue',
3766 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003767 parser.add_option('-j', '--json-file',
3768 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003769 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003770 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003771 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003772
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003773 issue = None
3774 if options.issue:
3775 try:
3776 issue = int(options.issue)
3777 except ValueError:
3778 DieWithError('A review issue id is expected to be a number')
3779
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003780 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003781
3782 if options.comment:
3783 cl.AddComment(options.comment)
3784 return 0
3785
3786 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003787 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003788 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003789 summary.append({
3790 'date': message['date'],
3791 'lgtm': False,
3792 'message': message['text'],
3793 'not_lgtm': False,
3794 'sender': message['sender'],
3795 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003796 if message['disapproval']:
3797 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003798 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003799 elif message['approval']:
3800 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003801 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003802 elif message['sender'] == data['owner_email']:
3803 color = Fore.MAGENTA
3804 else:
3805 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003806 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003807 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003808 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003809 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003810 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003811 if options.json_file:
3812 with open(options.json_file, 'wb') as f:
3813 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003814 return 0
3815
3816
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003817@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003818def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003819 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003820 parser.add_option('-d', '--display', action='store_true',
3821 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003822 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003823 help='New description to set for this issue (- for stdin, '
3824 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003825 parser.add_option('-f', '--force', action='store_true',
3826 help='Delete any unpublished Gerrit edits for this issue '
3827 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003828
3829 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003830 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003831 options, args = parser.parse_args(args)
3832 _process_codereview_select_options(parser, options)
3833
3834 target_issue = None
3835 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003836 target_issue = ParseIssueNumberArgument(args[0])
3837 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003838 parser.print_help()
3839 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003840
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003841 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003842
martiniss6eda05f2016-06-30 10:18:35 -07003843 kwargs = {
3844 'auth_config': auth_config,
3845 'codereview': options.forced_codereview,
3846 }
3847 if target_issue:
3848 kwargs['issue'] = target_issue.issue
3849 if options.forced_codereview == 'rietveld':
3850 kwargs['rietveld_server'] = target_issue.hostname
3851
3852 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003853
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003854 if not cl.GetIssue():
3855 DieWithError('This branch has no associated changelist.')
3856 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003857
smut@google.com34fb6b12015-07-13 20:03:26 +00003858 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003860 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003861
3862 if options.new_description:
3863 text = options.new_description
3864 if text == '-':
3865 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003866 elif text == '+':
3867 base_branch = cl.GetCommonAncestorWithUpstream()
3868 change = cl.GetChange(base_branch, None, local_description=True)
3869 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003870
3871 description.set_description(text)
3872 else:
3873 description.prompt()
3874
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003875 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003876 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003877 return 0
3878
3879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003880def CreateDescriptionFromLog(args):
3881 """Pulls out the commit log to use as a base for the CL description."""
3882 log_args = []
3883 if len(args) == 1 and not args[0].endswith('.'):
3884 log_args = [args[0] + '..']
3885 elif len(args) == 1 and args[0].endswith('...'):
3886 log_args = [args[0][:-1]]
3887 elif len(args) == 2:
3888 log_args = [args[0] + '..' + args[1]]
3889 else:
3890 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003891 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892
3893
thestig@chromium.org44202a22014-03-11 19:22:18 +00003894def CMDlint(parser, args):
3895 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003896 parser.add_option('--filter', action='append', metavar='-x,+y',
3897 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003898 auth.add_auth_options(parser)
3899 options, args = parser.parse_args(args)
3900 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003901
3902 # Access to a protected member _XX of a client class
3903 # pylint: disable=W0212
3904 try:
3905 import cpplint
3906 import cpplint_chromium
3907 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003909 return 1
3910
3911 # Change the current working directory before calling lint so that it
3912 # shows the correct base.
3913 previous_cwd = os.getcwd()
3914 os.chdir(settings.GetRoot())
3915 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003916 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003917 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3918 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003919 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003921 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003922
3923 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003924 command = args + files
3925 if options.filter:
3926 command = ['--filter=' + ','.join(options.filter)] + command
3927 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003928
3929 white_regex = re.compile(settings.GetLintRegex())
3930 black_regex = re.compile(settings.GetLintIgnoreRegex())
3931 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3932 for filename in filenames:
3933 if white_regex.match(filename):
3934 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003935 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003936 else:
3937 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3938 extra_check_functions)
3939 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003940 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003941 finally:
3942 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003943 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003944 if cpplint._cpplint_state.error_count != 0:
3945 return 1
3946 return 0
3947
3948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003950 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003951 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003953 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003954 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003955 auth.add_auth_options(parser)
3956 options, args = parser.parse_args(args)
3957 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958
sbc@chromium.org71437c02015-04-09 19:29:40 +00003959 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003960 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003961 return 1
3962
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003963 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 if args:
3965 base_branch = args[0]
3966 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003967 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003968 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003969
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003970 cl.RunHook(
3971 committing=not options.upload,
3972 may_prompt=False,
3973 verbose=options.verbose,
3974 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003975 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003976
3977
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003978def GenerateGerritChangeId(message):
3979 """Returns Ixxxxxx...xxx change id.
3980
3981 Works the same way as
3982 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3983 but can be called on demand on all platforms.
3984
3985 The basic idea is to generate git hash of a state of the tree, original commit
3986 message, author/committer info and timestamps.
3987 """
3988 lines = []
3989 tree_hash = RunGitSilent(['write-tree'])
3990 lines.append('tree %s' % tree_hash.strip())
3991 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3992 if code == 0:
3993 lines.append('parent %s' % parent.strip())
3994 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3995 lines.append('author %s' % author.strip())
3996 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3997 lines.append('committer %s' % committer.strip())
3998 lines.append('')
3999 # Note: Gerrit's commit-hook actually cleans message of some lines and
4000 # whitespace. This code is not doing this, but it clearly won't decrease
4001 # entropy.
4002 lines.append(message)
4003 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4004 stdin='\n'.join(lines))
4005 return 'I%s' % change_hash.strip()
4006
4007
wittman@chromium.org455dc922015-01-26 20:15:50 +00004008def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4009 """Computes the remote branch ref to use for the CL.
4010
4011 Args:
4012 remote (str): The git remote for the CL.
4013 remote_branch (str): The git remote branch for the CL.
4014 target_branch (str): The target branch specified by the user.
4015 pending_prefix (str): The pending prefix from the settings.
4016 """
4017 if not (remote and remote_branch):
4018 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004019
wittman@chromium.org455dc922015-01-26 20:15:50 +00004020 if target_branch:
4021 # Cannonicalize branch references to the equivalent local full symbolic
4022 # refs, which are then translated into the remote full symbolic refs
4023 # below.
4024 if '/' not in target_branch:
4025 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4026 else:
4027 prefix_replacements = (
4028 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4029 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4030 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4031 )
4032 match = None
4033 for regex, replacement in prefix_replacements:
4034 match = re.search(regex, target_branch)
4035 if match:
4036 remote_branch = target_branch.replace(match.group(0), replacement)
4037 break
4038 if not match:
4039 # This is a branch path but not one we recognize; use as-is.
4040 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004041 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4042 # Handle the refs that need to land in different refs.
4043 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004044
wittman@chromium.org455dc922015-01-26 20:15:50 +00004045 # Create the true path to the remote branch.
4046 # Does the following translation:
4047 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4048 # * refs/remotes/origin/master -> refs/heads/master
4049 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4050 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4051 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4052 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4053 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4054 'refs/heads/')
4055 elif remote_branch.startswith('refs/remotes/branch-heads'):
4056 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4057 # If a pending prefix exists then replace refs/ with it.
4058 if pending_prefix:
4059 remote_branch = remote_branch.replace('refs/', pending_prefix)
4060 return remote_branch
4061
4062
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004063def cleanup_list(l):
4064 """Fixes a list so that comma separated items are put as individual items.
4065
4066 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4067 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4068 """
4069 items = sum((i.split(',') for i in l), [])
4070 stripped_items = (i.strip() for i in items)
4071 return sorted(filter(None, stripped_items))
4072
4073
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004074@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004075def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004076 """Uploads the current changelist to codereview.
4077
4078 Can skip dependency patchset uploads for a branch by running:
4079 git config branch.branch_name.skip-deps-uploads True
4080 To unset run:
4081 git config --unset branch.branch_name.skip-deps-uploads
4082 Can also set the above globally by using the --global flag.
4083 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004084 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4085 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004086 parser.add_option('--bypass-watchlists', action='store_true',
4087 dest='bypass_watchlists',
4088 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004089 parser.add_option('-f', action='store_true', dest='force',
4090 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004091 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004092 parser.add_option('-b', '--bug',
4093 help='pre-populate the bug number(s) for this issue. '
4094 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004095 parser.add_option('--message-file', dest='message_file',
4096 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004097 parser.add_option('-t', dest='title',
4098 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004099 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004100 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004101 help='reviewer email addresses')
4102 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004103 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004104 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004105 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004106 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004107 parser.add_option('--emulate_svn_auto_props',
4108 '--emulate-svn-auto-props',
4109 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004110 dest="emulate_svn_auto_props",
4111 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004112 parser.add_option('-c', '--use-commit-queue', action='store_true',
4113 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004114 parser.add_option('--private', action='store_true',
4115 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004116 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004117 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004118 metavar='TARGET',
4119 help='Apply CL to remote ref TARGET. ' +
4120 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004121 parser.add_option('--squash', action='store_true',
4122 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004123 parser.add_option('--no-squash', action='store_true',
4124 help='Don\'t squash multiple commits into one ' +
4125 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004126 parser.add_option('--topic', default=None,
4127 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004128 parser.add_option('--email', default=None,
4129 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004130 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4131 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004132 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4133 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004134 help='Send the patchset to do a CQ dry run right after '
4135 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004136 parser.add_option('--dependencies', action='store_true',
4137 help='Uploads CLs of all the local branches that depend on '
4138 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004139
rmistry@google.com2dd99862015-06-22 12:22:18 +00004140 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004141 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004142 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004143 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004144 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004145 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004146 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004147
sbc@chromium.org71437c02015-04-09 19:29:40 +00004148 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004149 return 1
4150
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004151 options.reviewers = cleanup_list(options.reviewers)
4152 options.cc = cleanup_list(options.cc)
4153
tandriib80458a2016-06-23 12:20:07 -07004154 if options.message_file:
4155 if options.message:
4156 parser.error('only one of --message and --message-file allowed.')
4157 options.message = gclient_utils.FileRead(options.message_file)
4158 options.message_file = None
4159
tandrii4d0545a2016-07-06 03:56:49 -07004160 if options.cq_dry_run and options.use_commit_queue:
4161 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4162
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004163 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4164 settings.GetIsGerrit()
4165
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004166 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004167 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004168
4169
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004170def IsSubmoduleMergeCommit(ref):
4171 # When submodules are added to the repo, we expect there to be a single
4172 # non-git-svn merge commit at remote HEAD with a signature comment.
4173 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004174 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004175 return RunGit(cmd) != ''
4176
4177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004179 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004181 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4182 upstream and closes the issue automatically and atomically.
4183
4184 Otherwise (in case of Rietveld):
4185 Squashes branch into a single commit.
4186 Updates changelog with metadata (e.g. pointer to review).
4187 Pushes/dcommits the code upstream.
4188 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004189 """
4190 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4191 help='bypass upload presubmit hook')
4192 parser.add_option('-m', dest='message',
4193 help="override review description")
4194 parser.add_option('-f', action='store_true', dest='force',
4195 help="force yes to questions (don't prompt)")
4196 parser.add_option('-c', dest='contributor',
4197 help="external contributor for patch (appended to " +
4198 "description and used as author for git). Should be " +
4199 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004200 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004201 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004203 auth_config = auth.extract_auth_config_from_options(options)
4204
4205 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004206
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004207 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4208 if cl.IsGerrit():
4209 if options.message:
4210 # This could be implemented, but it requires sending a new patch to
4211 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4212 # Besides, Gerrit has the ability to change the commit message on submit
4213 # automatically, thus there is no need to support this option (so far?).
4214 parser.error('-m MESSAGE option is not supported for Gerrit.')
4215 if options.contributor:
4216 parser.error(
4217 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4218 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4219 'the contributor\'s "name <email>". If you can\'t upload such a '
4220 'commit for review, contact your repository admin and request'
4221 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004222 if not cl.GetIssue():
4223 DieWithError('You must upload the issue first to Gerrit.\n'
4224 ' If you would rather have `git cl land` upload '
4225 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004226 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4227 options.verbose)
4228
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004229 current = cl.GetBranch()
4230 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4231 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004232 print()
4233 print('Attempting to push branch %r into another local branch!' % current)
4234 print()
4235 print('Either reparent this branch on top of origin/master:')
4236 print(' git reparent-branch --root')
4237 print()
4238 print('OR run `git rebase-update` if you think the parent branch is ')
4239 print('already committed.')
4240 print()
4241 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004242 return 1
4243
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004244 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245 # Default to merging against our best guess of the upstream branch.
4246 args = [cl.GetUpstreamBranch()]
4247
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004248 if options.contributor:
4249 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004251 return 1
4252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004253 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004254 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255
sbc@chromium.org71437c02015-04-09 19:29:40 +00004256 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257 return 1
4258
4259 # This rev-list syntax means "show all commits not in my branch that
4260 # are in base_branch".
4261 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4262 base_branch]).splitlines()
4263 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004264 print('Base branch "%s" has %d commits '
4265 'not in this branch.' % (base_branch, len(upstream_commits)))
4266 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267 return 1
4268
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004269 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004270 svn_head = None
4271 if cmd == 'dcommit' or base_has_submodules:
4272 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4273 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004274
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004276 # If the base_head is a submodule merge commit, the first parent of the
4277 # base_head should be a git-svn commit, which is what we're interested in.
4278 base_svn_head = base_branch
4279 if base_has_submodules:
4280 base_svn_head += '^1'
4281
4282 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004283 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004284 print('This branch has %d additional commits not upstreamed yet.'
4285 % len(extra_commits.splitlines()))
4286 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4287 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004288 return 1
4289
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004290 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004291 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004292 author = None
4293 if options.contributor:
4294 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004295 hook_results = cl.RunHook(
4296 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004297 may_prompt=not options.force,
4298 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004299 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004300 if not hook_results.should_continue():
4301 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004302
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004303 # Check the tree status if the tree status URL is set.
4304 status = GetTreeStatus()
4305 if 'closed' == status:
4306 print('The tree is closed. Please wait for it to reopen. Use '
4307 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4308 return 1
4309 elif 'unknown' == status:
4310 print('Unable to determine tree status. Please verify manually and '
4311 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4312 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004314 change_desc = ChangeDescription(options.message)
4315 if not change_desc.description and cl.GetIssue():
4316 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004317
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004318 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004319 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004320 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004321 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004322 print('No description set.')
4323 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004324 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004325
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004326 # Keep a separate copy for the commit message, because the commit message
4327 # contains the link to the Rietveld issue, while the Rietveld message contains
4328 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004329 # Keep a separate copy for the commit message.
4330 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004331 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004332
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004333 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004334 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004335 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004336 # after it. Add a period on a new line to circumvent this. Also add a space
4337 # before the period to make sure that Gitiles continues to correctly resolve
4338 # the URL.
4339 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004340 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004341 commit_desc.append_footer('Patch from %s.' % options.contributor)
4342
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004343 print('Description:')
4344 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004346 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004347 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004348 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004350 # We want to squash all this branch's commits into one commit with the proper
4351 # description. We do this by doing a "reset --soft" to the base branch (which
4352 # keeps the working copy the same), then dcommitting that. If origin/master
4353 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4354 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004356 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4357 # Delete the branches if they exist.
4358 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4359 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4360 result = RunGitWithCode(showref_cmd)
4361 if result[0] == 0:
4362 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004363
4364 # We might be in a directory that's present in this branch but not in the
4365 # trunk. Move up to the top of the tree so that git commands that expect a
4366 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004367 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004368 if rel_base_path:
4369 os.chdir(rel_base_path)
4370
4371 # Stuff our change into the merge branch.
4372 # We wrap in a try...finally block so if anything goes wrong,
4373 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004374 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004375 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004376 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004377 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004379 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004380 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004382 RunGit(
4383 [
4384 'commit', '--author', options.contributor,
4385 '-m', commit_desc.description,
4386 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004388 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004389 if base_has_submodules:
4390 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4391 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4392 RunGit(['checkout', CHERRY_PICK_BRANCH])
4393 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004394 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004395 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004396 mirror = settings.GetGitMirror(remote)
4397 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004398 pending_prefix = settings.GetPendingRefPrefix()
4399 if not pending_prefix or branch.startswith(pending_prefix):
4400 # If not using refs/pending/heads/* at all, or target ref is already set
4401 # to pending, then push to the target ref directly.
4402 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004403 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004404 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004405 else:
4406 # Cherry-pick the change on top of pending ref and then push it.
4407 assert branch.startswith('refs/'), branch
4408 assert pending_prefix[-1] == '/', pending_prefix
4409 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004410 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004411 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004412 if retcode == 0:
4413 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004414 else:
4415 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004416 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004417 'svn', 'dcommit',
4418 '-C%s' % options.similarity,
4419 '--no-rebase', '--rmdir',
4420 ]
4421 if settings.GetForceHttpsCommitUrl():
4422 # Allow forcing https commit URLs for some projects that don't allow
4423 # committing to http URLs (like Google Code).
4424 remote_url = cl.GetGitSvnRemoteUrl()
4425 if urlparse.urlparse(remote_url).scheme == 'http':
4426 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004427 cmd_args.append('--commit-url=%s' % remote_url)
4428 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004429 if 'Committed r' in output:
4430 revision = re.match(
4431 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4432 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004433 finally:
4434 # And then swap back to the original branch and clean up.
4435 RunGit(['checkout', '-q', cl.GetBranch()])
4436 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004437 if base_has_submodules:
4438 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004439
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004440 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004441 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004442 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004443
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004444 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004445 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004446 try:
4447 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4448 # We set pushed_to_pending to False, since it made it all the way to the
4449 # real ref.
4450 pushed_to_pending = False
4451 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004452 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004454 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004455 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004457 if not to_pending:
4458 if viewvc_url and revision:
4459 change_desc.append_footer(
4460 'Committed: %s%s' % (viewvc_url, revision))
4461 elif revision:
4462 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print('Closing issue '
4464 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004465 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004467 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004468 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004469 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004470 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004471 if options.bypass_hooks:
4472 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4473 else:
4474 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004475 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004476
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004477 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004478 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004479 print('The commit is in the pending queue (%s).' % pending_ref)
4480 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4481 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004482
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004483 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4484 if os.path.isfile(hook):
4485 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004486
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004487 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004488
4489
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004490def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004491 print()
4492 print('Waiting for commit to be landed on %s...' % real_ref)
4493 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004494 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4495 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004496 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004497
4498 loop = 0
4499 while True:
4500 sys.stdout.write('fetching (%d)... \r' % loop)
4501 sys.stdout.flush()
4502 loop += 1
4503
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004504 if mirror:
4505 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004506 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4507 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4508 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4509 for commit in commits.splitlines():
4510 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004511 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004512 return commit
4513
4514 current_rev = to_rev
4515
4516
tandriibf429402016-09-14 07:09:12 -07004517def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004518 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4519
4520 Returns:
4521 (retcode of last operation, output log of last operation).
4522 """
4523 assert pending_ref.startswith('refs/'), pending_ref
4524 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4525 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4526 code = 0
4527 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004528 max_attempts = 3
4529 attempts_left = max_attempts
4530 while attempts_left:
4531 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004532 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004533 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004534
4535 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004536 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004537 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004538 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004539 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004540 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004541 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004542 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004543 continue
4544
4545 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004546 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004547 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004548 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004549 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004550 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4551 'the following files have merge conflicts:' % pending_ref)
4552 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4553 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004554 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004555 return code, out
4556
4557 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004558 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004559 code, out = RunGitWithCode(
4560 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4561 if code == 0:
4562 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004563 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004564 return code, out
4565
vapiera7fbd5a2016-06-16 09:17:49 -07004566 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004567 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004568 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004569 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004570 print('Fatal push error. Make sure your .netrc credentials and git '
4571 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004572 return code, out
4573
vapiera7fbd5a2016-06-16 09:17:49 -07004574 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004575 return code, out
4576
4577
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004578def IsFatalPushFailure(push_stdout):
4579 """True if retrying push won't help."""
4580 return '(prohibited by Gerrit)' in push_stdout
4581
4582
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004583@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004584def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004585 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004586 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004587 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004588 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004589 message = """This repository appears to be a git-svn mirror, but we
4590don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004591 else:
4592 message = """This doesn't appear to be an SVN repository.
4593If your project has a true, writeable git repository, you probably want to run
4594'git cl land' instead.
4595If your project has a git mirror of an upstream SVN master, you probably need
4596to run 'git svn init'.
4597
4598Using the wrong command might cause your commit to appear to succeed, and the
4599review to be closed, without actually landing upstream. If you choose to
4600proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004601 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004602 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004603 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4604 'Please let us know of this project you are committing to:'
4605 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 return SendUpstream(parser, args, 'dcommit')
4607
4608
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004609@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004610def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004611 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004612 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004613 print('This appears to be an SVN repository.')
4614 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004615 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004616 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004617 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004618
4619
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004620@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004621def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004622 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 parser.add_option('-b', dest='newbranch',
4624 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004625 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004627 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4628 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004629 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004630 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004631 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004632 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004634 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004635
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004636
4637 group = optparse.OptionGroup(
4638 parser,
4639 'Options for continuing work on the current issue uploaded from a '
4640 'different clone (e.g. different machine). Must be used independently '
4641 'from the other options. No issue number should be specified, and the '
4642 'branch must have an issue number associated with it')
4643 group.add_option('--reapply', action='store_true', dest='reapply',
4644 help='Reset the branch and reapply the issue.\n'
4645 'CAUTION: This will undo any local changes in this '
4646 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004647
4648 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004649 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004650 parser.add_option_group(group)
4651
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004652 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004653 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004655 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004656 auth_config = auth.extract_auth_config_from_options(options)
4657
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004658
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004659 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004660 if options.newbranch:
4661 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004662 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004663 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004664
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004665 cl = Changelist(auth_config=auth_config,
4666 codereview=options.forced_codereview)
4667 if not cl.GetIssue():
4668 parser.error('current branch must have an associated issue')
4669
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004670 upstream = cl.GetUpstreamBranch()
4671 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004672 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004673
4674 RunGit(['reset', '--hard', upstream])
4675 if options.pull:
4676 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004677
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004678 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4679 options.directory)
4680
4681 if len(args) != 1 or not args[0]:
4682 parser.error('Must specify issue number or url')
4683
4684 # We don't want uncommitted changes mixed up with the patch.
4685 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004686 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004688 if options.newbranch:
4689 if options.force:
4690 RunGit(['branch', '-D', options.newbranch],
4691 stderr=subprocess2.PIPE, error_ok=True)
4692 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004693 elif not GetCurrentBranch():
4694 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004695
4696 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4697
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004698 if cl.IsGerrit():
4699 if options.reject:
4700 parser.error('--reject is not supported with Gerrit codereview.')
4701 if options.nocommit:
4702 parser.error('--nocommit is not supported with Gerrit codereview.')
4703 if options.directory:
4704 parser.error('--directory is not supported with Gerrit codereview.')
4705
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004706 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004707 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004708
4709
4710def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004711 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004712 # Provide a wrapper for git svn rebase to help avoid accidental
4713 # git svn dcommit.
4714 # It's the only command that doesn't use parser at all since we just defer
4715 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004716
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004717 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004718
4719
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004720def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721 """Fetches the tree status and returns either 'open', 'closed',
4722 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004723 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004724 if url:
4725 status = urllib2.urlopen(url).read().lower()
4726 if status.find('closed') != -1 or status == '0':
4727 return 'closed'
4728 elif status.find('open') != -1 or status == '1':
4729 return 'open'
4730 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004731 return 'unset'
4732
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734def GetTreeStatusReason():
4735 """Fetches the tree status from a json url and returns the message
4736 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004737 url = settings.GetTreeStatusUrl()
4738 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004739 connection = urllib2.urlopen(json_url)
4740 status = json.loads(connection.read())
4741 connection.close()
4742 return status['message']
4743
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004745def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004746 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004747 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004748 status = GetTreeStatus()
4749 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004750 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004751 return 2
4752
vapiera7fbd5a2016-06-16 09:17:49 -07004753 print('The tree is %s' % status)
4754 print()
4755 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756 if status != 'open':
4757 return 1
4758 return 0
4759
4760
maruel@chromium.org15192402012-09-06 12:38:29 +00004761def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004762 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004763 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004764 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004765 '-b', '--bot', action='append',
4766 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4767 'times to specify multiple builders. ex: '
4768 '"-b win_rel -b win_layout". See '
4769 'the try server waterfall for the builders name and the tests '
4770 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004771 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004772 '-B', '--bucket', default='',
4773 help=('Buildbucket bucket to send the try requests.'))
4774 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004775 '-m', '--master', default='',
4776 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004777 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004778 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004779 help='Revision to use for the try job; default: the revision will '
4780 'be determined by the try recipe that builder runs, which usually '
4781 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004782 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004783 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004784 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004785 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004786 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004787 '--project',
4788 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004789 'in recipe to determine to which repository or directory to '
4790 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004791 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004792 '-p', '--property', dest='properties', action='append', default=[],
4793 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004794 'key2=value2 etc. The value will be treated as '
4795 'json if decodable, or as string otherwise. '
4796 'NOTE: using this may make your try job not usable for CQ, '
4797 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004798 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004799 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4800 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004801 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004802 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004803 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004804 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004805
machenbach@chromium.org45453142015-09-15 08:45:22 +00004806 # Make sure that all properties are prop=value pairs.
4807 bad_params = [x for x in options.properties if '=' not in x]
4808 if bad_params:
4809 parser.error('Got properties with missing "=": %s' % bad_params)
4810
maruel@chromium.org15192402012-09-06 12:38:29 +00004811 if args:
4812 parser.error('Unknown arguments: %s' % args)
4813
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004814 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004815 if not cl.GetIssue():
4816 parser.error('Need to upload first')
4817
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004818 if cl.IsGerrit():
4819 parser.error(
4820 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4821 'If your project has Commit Queue, dry run is a workaround:\n'
4822 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004823
tandriie113dfd2016-10-11 10:20:12 -07004824 error_message = cl.CannotTriggerTryJobReason()
4825 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004826 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004827
borenet6c0efe62016-10-19 08:13:29 -07004828 if options.bucket and options.master:
4829 parser.error('Only one of --bucket and --master may be used.')
4830
qyearsley1fdfcb62016-10-24 13:22:03 -07004831 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004832
qyearsleydd49f942016-10-28 11:57:22 -07004833 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4834 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004835 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004836 if options.verbose:
4837 print('git cl try with no bots now defaults to CQ Dry Run.')
4838 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004839
borenet6c0efe62016-10-19 08:13:29 -07004840 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004841 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004842 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004843 'of bot requires an initial job from a parent (usually a builder). '
4844 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004845 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004846 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004847
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004848 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004849 if patchset != cl.GetPatchset():
4850 print('Warning: Codereview server has newer patchsets (%s) than most '
4851 'recent upload from local checkout (%s). Did a previous upload '
4852 'fail?\n'
4853 'By default, git cl try uses the latest patchset from '
4854 'codereview, continuing to use patchset %s.\n' %
4855 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004856
tandrii568043b2016-10-11 07:49:18 -07004857 try:
borenet6c0efe62016-10-19 08:13:29 -07004858 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4859 patchset)
tandrii568043b2016-10-11 07:49:18 -07004860 except BuildbucketResponseException as ex:
4861 print('ERROR: %s' % ex)
4862 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004863 return 0
4864
4865
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004866def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004867 """Prints info about try jobs associated with current CL."""
4868 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004869 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004870 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004871 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004872 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004873 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004874 '--color', action='store_true', default=setup_color.IS_TTY,
4875 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004876 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004877 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4878 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004879 group.add_option(
4880 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004881 parser.add_option_group(group)
4882 auth.add_auth_options(parser)
4883 options, args = parser.parse_args(args)
4884 if args:
4885 parser.error('Unrecognized args: %s' % ' '.join(args))
4886
4887 auth_config = auth.extract_auth_config_from_options(options)
4888 cl = Changelist(auth_config=auth_config)
4889 if not cl.GetIssue():
4890 parser.error('Need to upload first')
4891
tandrii221ab252016-10-06 08:12:04 -07004892 patchset = options.patchset
4893 if not patchset:
4894 patchset = cl.GetMostRecentPatchset()
4895 if not patchset:
4896 parser.error('Codereview doesn\'t know about issue %s. '
4897 'No access to issue or wrong issue number?\n'
4898 'Either upload first, or pass --patchset explicitely' %
4899 cl.GetIssue())
4900
4901 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004902 print('Warning: Codereview server has newer patchsets (%s) than most '
4903 'recent upload from local checkout (%s). Did a previous upload '
4904 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004905 'By default, git cl try-results uses the latest patchset from '
4906 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004907 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004908 try:
tandrii221ab252016-10-06 08:12:04 -07004909 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004910 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004911 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004912 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004913 if options.json:
4914 write_try_results_json(options.json, jobs)
4915 else:
4916 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004917 return 0
4918
4919
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004920@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004921def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004922 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004923 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004924 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004925 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004927 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004928 if args:
4929 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004930 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004931 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004932 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004933 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004934
4935 # Clear configured merge-base, if there is one.
4936 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004937 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004938 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004939 return 0
4940
4941
thestig@chromium.org00858c82013-12-02 23:08:03 +00004942def CMDweb(parser, args):
4943 """Opens the current CL in the web browser."""
4944 _, args = parser.parse_args(args)
4945 if args:
4946 parser.error('Unrecognized args: %s' % ' '.join(args))
4947
4948 issue_url = Changelist().GetIssueURL()
4949 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004950 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004951 return 1
4952
4953 webbrowser.open(issue_url)
4954 return 0
4955
4956
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004957def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004958 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004959 parser.add_option('-d', '--dry-run', action='store_true',
4960 help='trigger in dry run mode')
4961 parser.add_option('-c', '--clear', action='store_true',
4962 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004963 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004964 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004965 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004966 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004967 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004968 if args:
4969 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004970 if options.dry_run and options.clear:
4971 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4972
iannuccie53c9352016-08-17 14:40:40 -07004973 cl = Changelist(auth_config=auth_config, issue=options.issue,
4974 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004975 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004976 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004977 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004978 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004979 state = _CQState.DRY_RUN
4980 else:
4981 state = _CQState.COMMIT
4982 if not cl.GetIssue():
4983 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004984 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004985 return 0
4986
4987
groby@chromium.org411034a2013-02-26 15:12:01 +00004988def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004989 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004990 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004991 auth.add_auth_options(parser)
4992 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004993 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004994 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004995 if args:
4996 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004997 cl = Changelist(auth_config=auth_config, issue=options.issue,
4998 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004999 # Ensure there actually is an issue to close.
5000 cl.GetDescription()
5001 cl.CloseIssue()
5002 return 0
5003
5004
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005005def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005006 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005007 parser.add_option(
5008 '--stat',
5009 action='store_true',
5010 dest='stat',
5011 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005012 auth.add_auth_options(parser)
5013 options, args = parser.parse_args(args)
5014 auth_config = auth.extract_auth_config_from_options(options)
5015 if args:
5016 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005017
5018 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005019 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005020 # Staged changes would be committed along with the patch from last
5021 # upload, hence counted toward the "last upload" side in the final
5022 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005023 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005024 return 1
5025
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005026 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005027 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005028 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005029 if not issue:
5030 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005031 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005032 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005033
5034 # Create a new branch based on the merge-base
5035 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005036 # Clear cached branch in cl object, to avoid overwriting original CL branch
5037 # properties.
5038 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005039 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005040 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005041 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005042 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005043 return rtn
5044
wychen@chromium.org06928532015-02-03 02:11:29 +00005045 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005046 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005047 cmd = ['git', 'diff']
5048 if options.stat:
5049 cmd.append('--stat')
5050 cmd.extend([TMP_BRANCH, branch, '--'])
5051 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005052 finally:
5053 RunGit(['checkout', '-q', branch])
5054 RunGit(['branch', '-D', TMP_BRANCH])
5055
5056 return 0
5057
5058
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005059def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005060 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005061 parser.add_option(
5062 '--no-color',
5063 action='store_true',
5064 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005065 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005066 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005067 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005068
5069 author = RunGit(['config', 'user.email']).strip() or None
5070
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005071 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005072
5073 if args:
5074 if len(args) > 1:
5075 parser.error('Unknown args')
5076 base_branch = args[0]
5077 else:
5078 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005079 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005080
5081 change = cl.GetChange(base_branch, None)
5082 return owners_finder.OwnersFinder(
5083 [f.LocalPath() for f in
5084 cl.GetChange(base_branch, None).AffectedFiles()],
5085 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005086 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005087 disable_color=options.no_color).run()
5088
5089
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005090def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005091 """Generates a diff command."""
5092 # Generate diff for the current branch's changes.
5093 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5094 upstream_commit, '--' ]
5095
5096 if args:
5097 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005098 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005099 diff_cmd.append(arg)
5100 else:
5101 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005102
5103 return diff_cmd
5104
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005105def MatchingFileType(file_name, extensions):
5106 """Returns true if the file name ends with one of the given extensions."""
5107 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005108
enne@chromium.org555cfe42014-01-29 18:21:39 +00005109@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005110def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005111 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005112 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005113 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005114 parser.add_option('--full', action='store_true',
5115 help='Reformat the full content of all touched files')
5116 parser.add_option('--dry-run', action='store_true',
5117 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005118 parser.add_option('--python', action='store_true',
5119 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005120 parser.add_option('--diff', action='store_true',
5121 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005122 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005123
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005124 # git diff generates paths against the root of the repository. Change
5125 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005126 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005127 if rel_base_path:
5128 os.chdir(rel_base_path)
5129
digit@chromium.org29e47272013-05-17 17:01:46 +00005130 # Grab the merge-base commit, i.e. the upstream commit of the current
5131 # branch when it was created or the last time it was rebased. This is
5132 # to cover the case where the user may have called "git fetch origin",
5133 # moving the origin branch to a newer commit, but hasn't rebased yet.
5134 upstream_commit = None
5135 cl = Changelist()
5136 upstream_branch = cl.GetUpstreamBranch()
5137 if upstream_branch:
5138 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5139 upstream_commit = upstream_commit.strip()
5140
5141 if not upstream_commit:
5142 DieWithError('Could not find base commit for this branch. '
5143 'Are you in detached state?')
5144
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005145 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5146 diff_output = RunGit(changed_files_cmd)
5147 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005148 # Filter out files deleted by this CL
5149 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005150
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005151 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5152 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5153 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005154 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005155
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005156 top_dir = os.path.normpath(
5157 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5158
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005159 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5160 # formatted. This is used to block during the presubmit.
5161 return_value = 0
5162
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005163 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005164 # Locate the clang-format binary in the checkout
5165 try:
5166 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005167 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005168 DieWithError(e)
5169
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005170 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005171 cmd = [clang_format_tool]
5172 if not opts.dry_run and not opts.diff:
5173 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005174 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005175 if opts.diff:
5176 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005177 else:
5178 env = os.environ.copy()
5179 env['PATH'] = str(os.path.dirname(clang_format_tool))
5180 try:
5181 script = clang_format.FindClangFormatScriptInChromiumTree(
5182 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005183 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005184 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005185
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005186 cmd = [sys.executable, script, '-p0']
5187 if not opts.dry_run and not opts.diff:
5188 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005189
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005190 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5191 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005192
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005193 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5194 if opts.diff:
5195 sys.stdout.write(stdout)
5196 if opts.dry_run and len(stdout) > 0:
5197 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005198
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005199 # Similar code to above, but using yapf on .py files rather than clang-format
5200 # on C/C++ files
5201 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005202 yapf_tool = gclient_utils.FindExecutable('yapf')
5203 if yapf_tool is None:
5204 DieWithError('yapf not found in PATH')
5205
5206 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005207 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005208 cmd = [yapf_tool]
5209 if not opts.dry_run and not opts.diff:
5210 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005211 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005212 if opts.diff:
5213 sys.stdout.write(stdout)
5214 else:
5215 # TODO(sbc): yapf --lines mode still has some issues.
5216 # https://github.com/google/yapf/issues/154
5217 DieWithError('--python currently only works with --full')
5218
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005219 # Dart's formatter does not have the nice property of only operating on
5220 # modified chunks, so hard code full.
5221 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005222 try:
5223 command = [dart_format.FindDartFmtToolInChromiumTree()]
5224 if not opts.dry_run and not opts.diff:
5225 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005226 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005227
ppi@chromium.org6593d932016-03-03 15:41:15 +00005228 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005229 if opts.dry_run and stdout:
5230 return_value = 2
5231 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005232 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5233 'found in this checkout. Files in other languages are still '
5234 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005235
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005236 # Format GN build files. Always run on full build files for canonical form.
5237 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005238 cmd = ['gn', 'format' ]
5239 if opts.dry_run or opts.diff:
5240 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005241 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005242 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5243 shell=sys.platform == 'win32',
5244 cwd=top_dir)
5245 if opts.dry_run and gn_ret == 2:
5246 return_value = 2 # Not formatted.
5247 elif opts.diff and gn_ret == 2:
5248 # TODO this should compute and print the actual diff.
5249 print("This change has GN build file diff for " + gn_diff_file)
5250 elif gn_ret != 0:
5251 # For non-dry run cases (and non-2 return values for dry-run), a
5252 # nonzero error code indicates a failure, probably because the file
5253 # doesn't parse.
5254 DieWithError("gn format failed on " + gn_diff_file +
5255 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005256
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005257 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005258
5259
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005260@subcommand.usage('<codereview url or issue id>')
5261def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005262 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005263 _, args = parser.parse_args(args)
5264
5265 if len(args) != 1:
5266 parser.print_help()
5267 return 1
5268
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005269 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005270 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005271 parser.print_help()
5272 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005273 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005274
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005275 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005276 output = RunGit(['config', '--local', '--get-regexp',
5277 r'branch\..*\.%s' % issueprefix],
5278 error_ok=True)
5279 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005280 if issue == target_issue:
5281 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005282
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005283 branches = []
5284 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005285 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005286 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005287 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005288 return 1
5289 if len(branches) == 1:
5290 RunGit(['checkout', branches[0]])
5291 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005292 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005293 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005294 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005295 which = raw_input('Choose by index: ')
5296 try:
5297 RunGit(['checkout', branches[int(which)]])
5298 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005299 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005300 return 1
5301
5302 return 0
5303
5304
maruel@chromium.org29404b52014-09-08 22:58:00 +00005305def CMDlol(parser, args):
5306 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005307 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005308 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5309 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5310 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005311 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005312 return 0
5313
5314
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005315class OptionParser(optparse.OptionParser):
5316 """Creates the option parse and add --verbose support."""
5317 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005318 optparse.OptionParser.__init__(
5319 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005320 self.add_option(
5321 '-v', '--verbose', action='count', default=0,
5322 help='Use 2 times for more debugging info')
5323
5324 def parse_args(self, args=None, values=None):
5325 options, args = optparse.OptionParser.parse_args(self, args, values)
5326 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5327 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5328 return options, args
5329
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005331def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005332 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005333 print('\nYour python version %s is unsupported, please upgrade.\n' %
5334 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005335 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005336
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005337 # Reload settings.
5338 global settings
5339 settings = Settings()
5340
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005341 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005342 dispatcher = subcommand.CommandDispatcher(__name__)
5343 try:
5344 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005345 except auth.AuthenticationError as e:
5346 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005347 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005348 if e.code != 500:
5349 raise
5350 DieWithError(
5351 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5352 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005353 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005354
5355
5356if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005357 # These affect sys.stdout so do it outside of main() to simplify mocks in
5358 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005359 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005360 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005361 try:
5362 sys.exit(main(sys.argv[1:]))
5363 except KeyboardInterrupt:
5364 sys.stderr.write('interrupted\n')
5365 sys.exit(1)