blob: 51593f20ec52fc0a99f65b70b859fcd1fa01a807 [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
agable32978d92016-11-01 12:55:02 -07002500 def _GetChangeCommit(self, issue=None):
2501 issue = issue or self.GetIssue()
2502 assert issue, 'issue is required to query Gerrit'
2503 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2504 if not data:
2505 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2506 return data
2507
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002508 def CMDLand(self, force, bypass_hooks, verbose):
2509 if git_common.is_dirty_git_tree('land'):
2510 return 1
tandriid60367b2016-06-22 05:25:12 -07002511 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2512 if u'Commit-Queue' in detail.get('labels', {}):
2513 if not force:
2514 ask_for_data('\nIt seems this repository has a Commit Queue, '
2515 'which can test and land changes for you. '
2516 'Are you sure you wish to bypass it?\n'
2517 'Press Enter to continue, Ctrl+C to abort.')
2518
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002519 differs = True
tandriic4344b52016-08-29 06:04:54 -07002520 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002521 # Note: git diff outputs nothing if there is no diff.
2522 if not last_upload or RunGit(['diff', last_upload]).strip():
2523 print('WARNING: some changes from local branch haven\'t been uploaded')
2524 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002525 if detail['current_revision'] == last_upload:
2526 differs = False
2527 else:
2528 print('WARNING: local branch contents differ from latest uploaded '
2529 'patchset')
2530 if differs:
2531 if not force:
2532 ask_for_data(
2533 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2534 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2535 elif not bypass_hooks:
2536 hook_results = self.RunHook(
2537 committing=True,
2538 may_prompt=not force,
2539 verbose=verbose,
2540 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2541 if not hook_results.should_continue():
2542 return 1
2543
2544 self.SubmitIssue(wait_for_merge=True)
2545 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002546 links = self._GetChangeCommit().get('web_links', [])
2547 for link in links:
2548 if link.get('name') == 'gerrit' and link.get('url'):
2549 print('Landed as %s' % link.get('url'))
2550 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002551 return 0
2552
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002553 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2554 directory):
2555 assert not reject
2556 assert not nocommit
2557 assert not directory
2558 assert parsed_issue_arg.valid
2559
2560 self._changelist.issue = parsed_issue_arg.issue
2561
2562 if parsed_issue_arg.hostname:
2563 self._gerrit_host = parsed_issue_arg.hostname
2564 self._gerrit_server = 'https://%s' % self._gerrit_host
2565
tandriic2405f52016-10-10 08:13:15 -07002566 try:
2567 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2568 except GerritIssueNotExists as e:
2569 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002570
2571 if not parsed_issue_arg.patchset:
2572 # Use current revision by default.
2573 revision_info = detail['revisions'][detail['current_revision']]
2574 patchset = int(revision_info['_number'])
2575 else:
2576 patchset = parsed_issue_arg.patchset
2577 for revision_info in detail['revisions'].itervalues():
2578 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2579 break
2580 else:
2581 DieWithError('Couldn\'t find patchset %i in issue %i' %
2582 (parsed_issue_arg.patchset, self.GetIssue()))
2583
2584 fetch_info = revision_info['fetch']['http']
2585 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2586 RunGit(['cherry-pick', 'FETCH_HEAD'])
2587 self.SetIssue(self.GetIssue())
2588 self.SetPatchset(patchset)
2589 print('Committed patch for issue %i pathset %i locally' %
2590 (self.GetIssue(), self.GetPatchset()))
2591 return 0
2592
2593 @staticmethod
2594 def ParseIssueURL(parsed_url):
2595 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2596 return None
2597 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2598 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2599 # Short urls like https://domain/<issue_number> can be used, but don't allow
2600 # specifying the patchset (you'd 404), but we allow that here.
2601 if parsed_url.path == '/':
2602 part = parsed_url.fragment
2603 else:
2604 part = parsed_url.path
2605 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2606 if match:
2607 return _ParsedIssueNumberArgument(
2608 issue=int(match.group(2)),
2609 patchset=int(match.group(4)) if match.group(4) else None,
2610 hostname=parsed_url.netloc)
2611 return None
2612
tandrii16e0b4e2016-06-07 10:34:28 -07002613 def _GerritCommitMsgHookCheck(self, offer_removal):
2614 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2615 if not os.path.exists(hook):
2616 return
2617 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2618 # custom developer made one.
2619 data = gclient_utils.FileRead(hook)
2620 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2621 return
2622 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002623 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002624 'and may interfere with it in subtle ways.\n'
2625 'We recommend you remove the commit-msg hook.')
2626 if offer_removal:
2627 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2628 if reply.lower().startswith('y'):
2629 gclient_utils.rm_file_or_tree(hook)
2630 print('Gerrit commit-msg hook removed.')
2631 else:
2632 print('OK, will keep Gerrit commit-msg hook in place.')
2633
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002634 def CMDUploadChange(self, options, args, change):
2635 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002636 if options.squash and options.no_squash:
2637 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002638
2639 if not options.squash and not options.no_squash:
2640 # Load default for user, repo, squash=true, in this order.
2641 options.squash = settings.GetSquashGerritUploads()
2642 elif options.no_squash:
2643 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002644
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002645 # We assume the remote called "origin" is the one we want.
2646 # It is probably not worthwhile to support different workflows.
2647 gerrit_remote = 'origin'
2648
2649 remote, remote_branch = self.GetRemoteBranch()
2650 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2651 pending_prefix='')
2652
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002653 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002654 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002655 if self.GetIssue():
2656 # Try to get the message from a previous upload.
2657 message = self.GetDescription()
2658 if not message:
2659 DieWithError(
2660 'failed to fetch description from current Gerrit issue %d\n'
2661 '%s' % (self.GetIssue(), self.GetIssueURL()))
2662 change_id = self._GetChangeDetail()['change_id']
2663 while True:
2664 footer_change_ids = git_footers.get_footer_change_id(message)
2665 if footer_change_ids == [change_id]:
2666 break
2667 if not footer_change_ids:
2668 message = git_footers.add_footer_change_id(message, change_id)
2669 print('WARNING: appended missing Change-Id to issue description')
2670 continue
2671 # There is already a valid footer but with different or several ids.
2672 # Doing this automatically is non-trivial as we don't want to lose
2673 # existing other footers, yet we want to append just 1 desired
2674 # Change-Id. Thus, just create a new footer, but let user verify the
2675 # new description.
2676 message = '%s\n\nChange-Id: %s' % (message, change_id)
2677 print(
2678 'WARNING: issue %s has Change-Id footer(s):\n'
2679 ' %s\n'
2680 'but issue has Change-Id %s, according to Gerrit.\n'
2681 'Please, check the proposed correction to the description, '
2682 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2683 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2684 change_id))
2685 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2686 if not options.force:
2687 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002688 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 message = change_desc.description
2690 if not message:
2691 DieWithError("Description is empty. Aborting...")
2692 # Continue the while loop.
2693 # Sanity check of this code - we should end up with proper message
2694 # footer.
2695 assert [change_id] == git_footers.get_footer_change_id(message)
2696 change_desc = ChangeDescription(message)
2697 else:
2698 change_desc = ChangeDescription(
2699 options.message or CreateDescriptionFromLog(args))
2700 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002701 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002702 if not change_desc.description:
2703 DieWithError("Description is empty. Aborting...")
2704 message = change_desc.description
2705 change_ids = git_footers.get_footer_change_id(message)
2706 if len(change_ids) > 1:
2707 DieWithError('too many Change-Id footers, at most 1 allowed.')
2708 if not change_ids:
2709 # Generate the Change-Id automatically.
2710 message = git_footers.add_footer_change_id(
2711 message, GenerateGerritChangeId(message))
2712 change_desc.set_description(message)
2713 change_ids = git_footers.get_footer_change_id(message)
2714 assert len(change_ids) == 1
2715 change_id = change_ids[0]
2716
2717 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2718 if remote is '.':
2719 # If our upstream branch is local, we base our squashed commit on its
2720 # squashed version.
2721 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2722 # Check the squashed hash of the parent.
2723 parent = RunGit(['config',
2724 'branch.%s.gerritsquashhash' % upstream_branch_name],
2725 error_ok=True).strip()
2726 # Verify that the upstream branch has been uploaded too, otherwise
2727 # Gerrit will create additional CLs when uploading.
2728 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2729 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002730 DieWithError(
2731 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002732 'Note: maybe you\'ve uploaded it with --no-squash. '
2733 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002734 ' git cl upload --squash\n' % upstream_branch_name)
2735 else:
2736 parent = self.GetCommonAncestorWithUpstream()
2737
2738 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2739 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2740 '-m', message]).strip()
2741 else:
2742 change_desc = ChangeDescription(
2743 options.message or CreateDescriptionFromLog(args))
2744 if not change_desc.description:
2745 DieWithError("Description is empty. Aborting...")
2746
2747 if not git_footers.get_footer_change_id(change_desc.description):
2748 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002749 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2750 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751 ref_to_push = 'HEAD'
2752 parent = '%s/%s' % (gerrit_remote, branch)
2753 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2754
2755 assert change_desc
2756 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2757 ref_to_push)]).splitlines()
2758 if len(commits) > 1:
2759 print('WARNING: This will upload %d commits. Run the following command '
2760 'to see which commits will be uploaded: ' % len(commits))
2761 print('git log %s..%s' % (parent, ref_to_push))
2762 print('You can also use `git squash-branch` to squash these into a '
2763 'single commit.')
2764 ask_for_data('About to upload; enter to confirm.')
2765
2766 if options.reviewers or options.tbr_owners:
2767 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2768 change)
2769
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002770 # Extra options that can be specified at push time. Doc:
2771 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2772 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002773 if change_desc.get_reviewers(tbr_only=True):
2774 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2775 refspec_opts.append('l=Code-Review+1')
2776
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002777 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002778 if not re.match(r'^[\w ]+$', options.title):
2779 options.title = re.sub(r'[^\w ]', '', options.title)
2780 print('WARNING: Patchset title may only contain alphanumeric chars '
2781 'and spaces. Cleaned up title:\n%s' % options.title)
2782 if not options.force:
2783 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002784 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2785 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002786 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2787
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002788 if options.send_mail:
2789 if not change_desc.get_reviewers():
2790 DieWithError('Must specify reviewers to send email.')
2791 refspec_opts.append('notify=ALL')
2792 else:
2793 refspec_opts.append('notify=NONE')
2794
tandrii99a72f22016-08-17 14:33:24 -07002795 reviewers = change_desc.get_reviewers()
2796 if reviewers:
2797 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002798
agablec6787972016-09-09 16:13:34 -07002799 if options.private:
2800 refspec_opts.append('draft')
2801
rmistry9eadede2016-09-19 11:22:43 -07002802 if options.topic:
2803 # Documentation on Gerrit topics is here:
2804 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2805 refspec_opts.append('topic=%s' % options.topic)
2806
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002807 refspec_suffix = ''
2808 if refspec_opts:
2809 refspec_suffix = '%' + ','.join(refspec_opts)
2810 assert ' ' not in refspec_suffix, (
2811 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002812 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002814 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002815 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002816 print_stdout=True,
2817 # Flush after every line: useful for seeing progress when running as
2818 # recipe.
2819 filter_fn=lambda _: sys.stdout.flush())
2820
2821 if options.squash:
2822 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2823 change_numbers = [m.group(1)
2824 for m in map(regex.match, push_stdout.splitlines())
2825 if m]
2826 if len(change_numbers) != 1:
2827 DieWithError(
2828 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2829 'Change-Id: %s') % (len(change_numbers), change_id))
2830 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002831 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002832
2833 # Add cc's from the CC_LIST and --cc flag (if any).
2834 cc = self.GetCCList().split(',')
2835 if options.cc:
2836 cc.extend(options.cc)
2837 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002838 if change_desc.get_cced():
2839 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002840 if cc:
2841 gerrit_util.AddReviewers(
2842 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2843
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 return 0
2845
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002846 def _AddChangeIdToCommitMessage(self, options, args):
2847 """Re-commits using the current message, assumes the commit hook is in
2848 place.
2849 """
2850 log_desc = options.message or CreateDescriptionFromLog(args)
2851 git_command = ['commit', '--amend', '-m', log_desc]
2852 RunGit(git_command)
2853 new_log_desc = CreateDescriptionFromLog(args)
2854 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002855 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002856 return new_log_desc
2857 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002858 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002859
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002860 def SetCQState(self, new_state):
2861 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002862 vote_map = {
2863 _CQState.NONE: 0,
2864 _CQState.DRY_RUN: 1,
2865 _CQState.COMMIT : 2,
2866 }
2867 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2868 labels={'Commit-Queue': vote_map[new_state]})
2869
tandriie113dfd2016-10-11 10:20:12 -07002870 def CannotTriggerTryJobReason(self):
2871 # TODO(tandrii): implement for Gerrit.
2872 raise NotImplementedError()
2873
tandriide281ae2016-10-12 06:02:30 -07002874 def GetIssueOwner(self):
2875 # TODO(tandrii): implement for Gerrit.
2876 raise NotImplementedError()
2877
2878 def GetIssueProject(self):
2879 # TODO(tandrii): implement for Gerrit.
2880 raise NotImplementedError()
2881
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002882
2883_CODEREVIEW_IMPLEMENTATIONS = {
2884 'rietveld': _RietveldChangelistImpl,
2885 'gerrit': _GerritChangelistImpl,
2886}
2887
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002888
iannuccie53c9352016-08-17 14:40:40 -07002889def _add_codereview_issue_select_options(parser, extra=""):
2890 _add_codereview_select_options(parser)
2891
2892 text = ('Operate on this issue number instead of the current branch\'s '
2893 'implicit issue.')
2894 if extra:
2895 text += ' '+extra
2896 parser.add_option('-i', '--issue', type=int, help=text)
2897
2898
2899def _process_codereview_issue_select_options(parser, options):
2900 _process_codereview_select_options(parser, options)
2901 if options.issue is not None and not options.forced_codereview:
2902 parser.error('--issue must be specified with either --rietveld or --gerrit')
2903
2904
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002905def _add_codereview_select_options(parser):
2906 """Appends --gerrit and --rietveld options to force specific codereview."""
2907 parser.codereview_group = optparse.OptionGroup(
2908 parser, 'EXPERIMENTAL! Codereview override options')
2909 parser.add_option_group(parser.codereview_group)
2910 parser.codereview_group.add_option(
2911 '--gerrit', action='store_true',
2912 help='Force the use of Gerrit for codereview')
2913 parser.codereview_group.add_option(
2914 '--rietveld', action='store_true',
2915 help='Force the use of Rietveld for codereview')
2916
2917
2918def _process_codereview_select_options(parser, options):
2919 if options.gerrit and options.rietveld:
2920 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2921 options.forced_codereview = None
2922 if options.gerrit:
2923 options.forced_codereview = 'gerrit'
2924 elif options.rietveld:
2925 options.forced_codereview = 'rietveld'
2926
2927
tandriif9aefb72016-07-01 09:06:51 -07002928def _get_bug_line_values(default_project, bugs):
2929 """Given default_project and comma separated list of bugs, yields bug line
2930 values.
2931
2932 Each bug can be either:
2933 * a number, which is combined with default_project
2934 * string, which is left as is.
2935
2936 This function may produce more than one line, because bugdroid expects one
2937 project per line.
2938
2939 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2940 ['v8:123', 'chromium:789']
2941 """
2942 default_bugs = []
2943 others = []
2944 for bug in bugs.split(','):
2945 bug = bug.strip()
2946 if bug:
2947 try:
2948 default_bugs.append(int(bug))
2949 except ValueError:
2950 others.append(bug)
2951
2952 if default_bugs:
2953 default_bugs = ','.join(map(str, default_bugs))
2954 if default_project:
2955 yield '%s:%s' % (default_project, default_bugs)
2956 else:
2957 yield default_bugs
2958 for other in sorted(others):
2959 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2960 yield other
2961
2962
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002963class ChangeDescription(object):
2964 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002965 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002966 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002967 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002968
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002969 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002970 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002971
agable@chromium.org42c20792013-09-12 17:34:49 +00002972 @property # www.logilab.org/ticket/89786
2973 def description(self): # pylint: disable=E0202
2974 return '\n'.join(self._description_lines)
2975
2976 def set_description(self, desc):
2977 if isinstance(desc, basestring):
2978 lines = desc.splitlines()
2979 else:
2980 lines = [line.rstrip() for line in desc]
2981 while lines and not lines[0]:
2982 lines.pop(0)
2983 while lines and not lines[-1]:
2984 lines.pop(-1)
2985 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002986
piman@chromium.org336f9122014-09-04 02:16:55 +00002987 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002988 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002989 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002990 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002991 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002992 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002993
agable@chromium.org42c20792013-09-12 17:34:49 +00002994 # Get the set of R= and TBR= lines and remove them from the desciption.
2995 regexp = re.compile(self.R_LINE)
2996 matches = [regexp.match(line) for line in self._description_lines]
2997 new_desc = [l for i, l in enumerate(self._description_lines)
2998 if not matches[i]]
2999 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003000
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 # Construct new unified R= and TBR= lines.
3002 r_names = []
3003 tbr_names = []
3004 for match in matches:
3005 if not match:
3006 continue
3007 people = cleanup_list([match.group(2).strip()])
3008 if match.group(1) == 'TBR':
3009 tbr_names.extend(people)
3010 else:
3011 r_names.extend(people)
3012 for name in r_names:
3013 if name not in reviewers:
3014 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003015 if add_owners_tbr:
3016 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003017 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003018 all_reviewers = set(tbr_names + reviewers)
3019 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3020 all_reviewers)
3021 tbr_names.extend(owners_db.reviewers_for(missing_files,
3022 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3024 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3025
3026 # Put the new lines in the description where the old first R= line was.
3027 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3028 if 0 <= line_loc < len(self._description_lines):
3029 if new_tbr_line:
3030 self._description_lines.insert(line_loc, new_tbr_line)
3031 if new_r_line:
3032 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003033 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003034 if new_r_line:
3035 self.append_footer(new_r_line)
3036 if new_tbr_line:
3037 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003038
tandriif9aefb72016-07-01 09:06:51 -07003039 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003040 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003041 self.set_description([
3042 '# Enter a description of the change.',
3043 '# This will be displayed on the codereview site.',
3044 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003045 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003046 '--------------------',
3047 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003048
agable@chromium.org42c20792013-09-12 17:34:49 +00003049 regexp = re.compile(self.BUG_LINE)
3050 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003051 prefix = settings.GetBugPrefix()
3052 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3053 for value in values:
3054 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3055 self.append_footer('BUG=%s' % value)
3056
agable@chromium.org42c20792013-09-12 17:34:49 +00003057 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003058 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003059 if not content:
3060 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003061 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003062
3063 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3065 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003066 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003067 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003068
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003070 """Adds a footer line to the description.
3071
3072 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3073 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3074 that Gerrit footers are always at the end.
3075 """
3076 parsed_footer_line = git_footers.parse_footer(line)
3077 if parsed_footer_line:
3078 # Line is a gerrit footer in the form: Footer-Key: any value.
3079 # Thus, must be appended observing Gerrit footer rules.
3080 self.set_description(
3081 git_footers.add_footer(self.description,
3082 key=parsed_footer_line[0],
3083 value=parsed_footer_line[1]))
3084 return
3085
3086 if not self._description_lines:
3087 self._description_lines.append(line)
3088 return
3089
3090 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3091 if gerrit_footers:
3092 # git_footers.split_footers ensures that there is an empty line before
3093 # actual (gerrit) footers, if any. We have to keep it that way.
3094 assert top_lines and top_lines[-1] == ''
3095 top_lines, separator = top_lines[:-1], top_lines[-1:]
3096 else:
3097 separator = [] # No need for separator if there are no gerrit_footers.
3098
3099 prev_line = top_lines[-1] if top_lines else ''
3100 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3101 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3102 top_lines.append('')
3103 top_lines.append(line)
3104 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003105
tandrii99a72f22016-08-17 14:33:24 -07003106 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003107 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003108 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003109 reviewers = [match.group(2).strip()
3110 for match in matches
3111 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003112 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003113
bradnelsond975b302016-10-23 12:20:23 -07003114 def get_cced(self):
3115 """Retrieves the list of reviewers."""
3116 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3117 cced = [match.group(2).strip() for match in matches if match]
3118 return cleanup_list(cced)
3119
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003120
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003121def get_approving_reviewers(props):
3122 """Retrieves the reviewers that approved a CL from the issue properties with
3123 messages.
3124
3125 Note that the list may contain reviewers that are not committer, thus are not
3126 considered by the CQ.
3127 """
3128 return sorted(
3129 set(
3130 message['sender']
3131 for message in props['messages']
3132 if message['approval'] and message['sender'] in props['reviewers']
3133 )
3134 )
3135
3136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003137def FindCodereviewSettingsFile(filename='codereview.settings'):
3138 """Finds the given file starting in the cwd and going up.
3139
3140 Only looks up to the top of the repository unless an
3141 'inherit-review-settings-ok' file exists in the root of the repository.
3142 """
3143 inherit_ok_file = 'inherit-review-settings-ok'
3144 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003145 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3147 root = '/'
3148 while True:
3149 if filename in os.listdir(cwd):
3150 if os.path.isfile(os.path.join(cwd, filename)):
3151 return open(os.path.join(cwd, filename))
3152 if cwd == root:
3153 break
3154 cwd = os.path.dirname(cwd)
3155
3156
3157def LoadCodereviewSettingsFromFile(fileobj):
3158 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003159 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003160
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003161 def SetProperty(name, setting, unset_error_ok=False):
3162 fullname = 'rietveld.' + name
3163 if setting in keyvals:
3164 RunGit(['config', fullname, keyvals[setting]])
3165 else:
3166 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3167
tandrii48df5812016-10-17 03:55:37 -07003168 if not keyvals.get('GERRIT_HOST', False):
3169 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003170 # Only server setting is required. Other settings can be absent.
3171 # In that case, we ignore errors raised during option deletion attempt.
3172 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003173 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003174 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3175 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003176 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003177 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003178 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3179 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003180 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003181 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003182 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003183 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003184 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3185 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003187 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003188 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003189
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003190 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003191 RunGit(['config', 'gerrit.squash-uploads',
3192 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003193
tandrii@chromium.org28253532016-04-14 13:46:56 +00003194 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003195 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003196 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3197
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003198 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3199 #should be of the form
3200 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3201 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3202 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3203 keyvals['ORIGIN_URL_CONFIG']])
3204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003206def urlretrieve(source, destination):
3207 """urllib is broken for SSL connections via a proxy therefore we
3208 can't use urllib.urlretrieve()."""
3209 with open(destination, 'w') as f:
3210 f.write(urllib2.urlopen(source).read())
3211
3212
ukai@chromium.org712d6102013-11-27 00:52:58 +00003213def hasSheBang(fname):
3214 """Checks fname is a #! script."""
3215 with open(fname) as f:
3216 return f.read(2).startswith('#!')
3217
3218
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003219# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3220def DownloadHooks(*args, **kwargs):
3221 pass
3222
3223
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003224def DownloadGerritHook(force):
3225 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003226
3227 Args:
3228 force: True to update hooks. False to install hooks if not present.
3229 """
3230 if not settings.GetIsGerrit():
3231 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003232 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003233 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3234 if not os.access(dst, os.X_OK):
3235 if os.path.exists(dst):
3236 if not force:
3237 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003238 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003239 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003240 if not hasSheBang(dst):
3241 DieWithError('Not a script: %s\n'
3242 'You need to download from\n%s\n'
3243 'into .git/hooks/commit-msg and '
3244 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003245 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3246 except Exception:
3247 if os.path.exists(dst):
3248 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003249 DieWithError('\nFailed to download hooks.\n'
3250 'You need to download from\n%s\n'
3251 'into .git/hooks/commit-msg and '
3252 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003253
3254
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003255
3256def GetRietveldCodereviewSettingsInteractively():
3257 """Prompt the user for settings."""
3258 server = settings.GetDefaultServerUrl(error_ok=True)
3259 prompt = 'Rietveld server (host[:port])'
3260 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3261 newserver = ask_for_data(prompt + ':')
3262 if not server and not newserver:
3263 newserver = DEFAULT_SERVER
3264 if newserver:
3265 newserver = gclient_utils.UpgradeToHttps(newserver)
3266 if newserver != server:
3267 RunGit(['config', 'rietveld.server', newserver])
3268
3269 def SetProperty(initial, caption, name, is_url):
3270 prompt = caption
3271 if initial:
3272 prompt += ' ("x" to clear) [%s]' % initial
3273 new_val = ask_for_data(prompt + ':')
3274 if new_val == 'x':
3275 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3276 elif new_val:
3277 if is_url:
3278 new_val = gclient_utils.UpgradeToHttps(new_val)
3279 if new_val != initial:
3280 RunGit(['config', 'rietveld.' + name, new_val])
3281
3282 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3283 SetProperty(settings.GetDefaultPrivateFlag(),
3284 'Private flag (rietveld only)', 'private', False)
3285 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3286 'tree-status-url', False)
3287 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3288 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3289 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3290 'run-post-upload-hook', False)
3291
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003292@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003294 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003295
tandrii5d0a0422016-09-14 06:24:35 -07003296 print('WARNING: git cl config works for Rietveld only')
3297 # TODO(tandrii): remove this once we switch to Gerrit.
3298 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003299 parser.add_option('--activate-update', action='store_true',
3300 help='activate auto-updating [rietveld] section in '
3301 '.git/config')
3302 parser.add_option('--deactivate-update', action='store_true',
3303 help='deactivate auto-updating [rietveld] section in '
3304 '.git/config')
3305 options, args = parser.parse_args(args)
3306
3307 if options.deactivate_update:
3308 RunGit(['config', 'rietveld.autoupdate', 'false'])
3309 return
3310
3311 if options.activate_update:
3312 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3313 return
3314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003315 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003316 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317 return 0
3318
3319 url = args[0]
3320 if not url.endswith('codereview.settings'):
3321 url = os.path.join(url, 'codereview.settings')
3322
3323 # Load code review settings and download hooks (if available).
3324 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3325 return 0
3326
3327
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003328def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003329 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003330 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3331 branch = ShortBranchName(branchref)
3332 _, args = parser.parse_args(args)
3333 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003334 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003335 return RunGit(['config', 'branch.%s.base-url' % branch],
3336 error_ok=False).strip()
3337 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003338 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003339 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3340 error_ok=False).strip()
3341
3342
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003343def color_for_status(status):
3344 """Maps a Changelist status to color, for CMDstatus and other tools."""
3345 return {
3346 'unsent': Fore.RED,
3347 'waiting': Fore.BLUE,
3348 'reply': Fore.YELLOW,
3349 'lgtm': Fore.GREEN,
3350 'commit': Fore.MAGENTA,
3351 'closed': Fore.CYAN,
3352 'error': Fore.WHITE,
3353 }.get(status, Fore.WHITE)
3354
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003355
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003356def get_cl_statuses(changes, fine_grained, max_processes=None):
3357 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003358
3359 If fine_grained is true, this will fetch CL statuses from the server.
3360 Otherwise, simply indicate if there's a matching url for the given branches.
3361
3362 If max_processes is specified, it is used as the maximum number of processes
3363 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3364 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003365
3366 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003367 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003368 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003369 upload.verbosity = 0
3370
3371 if fine_grained:
3372 # Process one branch synchronously to work through authentication, then
3373 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003374 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003375 def fetch(cl):
3376 try:
3377 return (cl, cl.GetStatus())
3378 except:
3379 # See http://crbug.com/629863.
3380 logging.exception('failed to fetch status for %s:', cl)
3381 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003382 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003383
tandriiea9514a2016-08-17 12:32:37 -07003384 changes_to_fetch = changes[1:]
3385 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003386 # Exit early if there was only one branch to fetch.
3387 return
3388
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003389 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003390 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003391 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003392 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003393
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003394 fetched_cls = set()
3395 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003396 while True:
3397 try:
3398 row = it.next(timeout=5)
3399 except multiprocessing.TimeoutError:
3400 break
3401
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003402 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003403 yield row
3404
3405 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003406 for cl in set(changes_to_fetch) - fetched_cls:
3407 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003408
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003409 else:
3410 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003411 for cl in changes:
3412 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003413
rmistry@google.com2dd99862015-06-22 12:22:18 +00003414
3415def upload_branch_deps(cl, args):
3416 """Uploads CLs of local branches that are dependents of the current branch.
3417
3418 If the local branch dependency tree looks like:
3419 test1 -> test2.1 -> test3.1
3420 -> test3.2
3421 -> test2.2 -> test3.3
3422
3423 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3424 run on the dependent branches in this order:
3425 test2.1, test3.1, test3.2, test2.2, test3.3
3426
3427 Note: This function does not rebase your local dependent branches. Use it when
3428 you make a change to the parent branch that will not conflict with its
3429 dependent branches, and you would like their dependencies updated in
3430 Rietveld.
3431 """
3432 if git_common.is_dirty_git_tree('upload-branch-deps'):
3433 return 1
3434
3435 root_branch = cl.GetBranch()
3436 if root_branch is None:
3437 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3438 'Get on a branch!')
3439 if not cl.GetIssue() or not cl.GetPatchset():
3440 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3441 'patchset dependencies without an uploaded CL.')
3442
3443 branches = RunGit(['for-each-ref',
3444 '--format=%(refname:short) %(upstream:short)',
3445 'refs/heads'])
3446 if not branches:
3447 print('No local branches found.')
3448 return 0
3449
3450 # Create a dictionary of all local branches to the branches that are dependent
3451 # on it.
3452 tracked_to_dependents = collections.defaultdict(list)
3453 for b in branches.splitlines():
3454 tokens = b.split()
3455 if len(tokens) == 2:
3456 branch_name, tracked = tokens
3457 tracked_to_dependents[tracked].append(branch_name)
3458
vapiera7fbd5a2016-06-16 09:17:49 -07003459 print()
3460 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003461 dependents = []
3462 def traverse_dependents_preorder(branch, padding=''):
3463 dependents_to_process = tracked_to_dependents.get(branch, [])
3464 padding += ' '
3465 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003467 dependents.append(dependent)
3468 traverse_dependents_preorder(dependent, padding)
3469 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003471
3472 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003474 return 0
3475
vapiera7fbd5a2016-06-16 09:17:49 -07003476 print('This command will checkout all dependent branches and run '
3477 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003478 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3479
andybons@chromium.org962f9462016-02-03 20:00:42 +00003480 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003481 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003482 args.extend(['-t', 'Updated patchset dependency'])
3483
rmistry@google.com2dd99862015-06-22 12:22:18 +00003484 # Record all dependents that failed to upload.
3485 failures = {}
3486 # Go through all dependents, checkout the branch and upload.
3487 try:
3488 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003489 print()
3490 print('--------------------------------------')
3491 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003492 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003493 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003494 try:
3495 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003496 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003497 failures[dependent_branch] = 1
3498 except: # pylint: disable=W0702
3499 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003500 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003501 finally:
3502 # Swap back to the original root branch.
3503 RunGit(['checkout', '-q', root_branch])
3504
vapiera7fbd5a2016-06-16 09:17:49 -07003505 print()
3506 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003507 for dependent_branch in dependents:
3508 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003509 print(' %s : %s' % (dependent_branch, upload_status))
3510 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003511
3512 return 0
3513
3514
kmarshall3bff56b2016-06-06 18:31:47 -07003515def CMDarchive(parser, args):
3516 """Archives and deletes branches associated with closed changelists."""
3517 parser.add_option(
3518 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003519 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003520 parser.add_option(
3521 '-f', '--force', action='store_true',
3522 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003523 parser.add_option(
3524 '-d', '--dry-run', action='store_true',
3525 help='Skip the branch tagging and removal steps.')
3526 parser.add_option(
3527 '-t', '--notags', action='store_true',
3528 help='Do not tag archived branches. '
3529 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003530
3531 auth.add_auth_options(parser)
3532 options, args = parser.parse_args(args)
3533 if args:
3534 parser.error('Unsupported args: %s' % ' '.join(args))
3535 auth_config = auth.extract_auth_config_from_options(options)
3536
3537 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3538 if not branches:
3539 return 0
3540
vapiera7fbd5a2016-06-16 09:17:49 -07003541 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003542 changes = [Changelist(branchref=b, auth_config=auth_config)
3543 for b in branches.splitlines()]
3544 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3545 statuses = get_cl_statuses(changes,
3546 fine_grained=True,
3547 max_processes=options.maxjobs)
3548 proposal = [(cl.GetBranch(),
3549 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3550 for cl, status in statuses
3551 if status == 'closed']
3552 proposal.sort()
3553
3554 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003556 return 0
3557
3558 current_branch = GetCurrentBranch()
3559
vapiera7fbd5a2016-06-16 09:17:49 -07003560 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003561 if options.notags:
3562 for next_item in proposal:
3563 print(' ' + next_item[0])
3564 else:
3565 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3566 for next_item in proposal:
3567 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003568
kmarshall9249e012016-08-23 12:02:16 -07003569 # Quit now on precondition failure or if instructed by the user, either
3570 # via an interactive prompt or by command line flags.
3571 if options.dry_run:
3572 print('\nNo changes were made (dry run).\n')
3573 return 0
3574 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003575 print('You are currently on a branch \'%s\' which is associated with a '
3576 'closed codereview issue, so archive cannot proceed. Please '
3577 'checkout another branch and run this command again.' %
3578 current_branch)
3579 return 1
kmarshall9249e012016-08-23 12:02:16 -07003580 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003581 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3582 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003583 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003584 return 1
3585
3586 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003587 if not options.notags:
3588 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003589 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003590
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003592
3593 return 0
3594
3595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003596def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003597 """Show status of changelists.
3598
3599 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003600 - Red not sent for review or broken
3601 - Blue waiting for review
3602 - Yellow waiting for you to reply to review
3603 - Green LGTM'ed
3604 - Magenta in the commit queue
3605 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003606
3607 Also see 'git cl comments'.
3608 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003609 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003610 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003611 parser.add_option('-f', '--fast', action='store_true',
3612 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003613 parser.add_option(
3614 '-j', '--maxjobs', action='store', type=int,
3615 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003616
3617 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003618 _add_codereview_issue_select_options(
3619 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003620 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003621 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003622 if args:
3623 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003624 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625
iannuccie53c9352016-08-17 14:40:40 -07003626 if options.issue is not None and not options.field:
3627 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003630 cl = Changelist(auth_config=auth_config, issue=options.issue,
3631 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003633 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634 elif options.field == 'id':
3635 issueid = cl.GetIssue()
3636 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003637 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638 elif options.field == 'patch':
3639 patchset = cl.GetPatchset()
3640 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003641 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003642 elif options.field == 'status':
3643 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644 elif options.field == 'url':
3645 url = cl.GetIssueURL()
3646 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003647 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003648 return 0
3649
3650 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3651 if not branches:
3652 print('No local branch found.')
3653 return 0
3654
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003655 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003656 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003657 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003658 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003659 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003660 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003661 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003662
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003663 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003664 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3665 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3666 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003667 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003668 c, status = output.next()
3669 branch_statuses[c.GetBranch()] = status
3670 status = branch_statuses.pop(branch)
3671 url = cl.GetIssueURL()
3672 if url and (not status or status == 'error'):
3673 # The issue probably doesn't exist anymore.
3674 url += ' (broken)'
3675
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003676 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003677 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003678 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003679 color = ''
3680 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003681 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003682 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003683 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003684 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003685
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003686 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003687 print()
3688 print('Current branch:',)
3689 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003690 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003691 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003692 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003693 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003694 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003695 print('Issue description:')
3696 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697 return 0
3698
3699
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003700def colorize_CMDstatus_doc():
3701 """To be called once in main() to add colors to git cl status help."""
3702 colors = [i for i in dir(Fore) if i[0].isupper()]
3703
3704 def colorize_line(line):
3705 for color in colors:
3706 if color in line.upper():
3707 # Extract whitespaces first and the leading '-'.
3708 indent = len(line) - len(line.lstrip(' ')) + 1
3709 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3710 return line
3711
3712 lines = CMDstatus.__doc__.splitlines()
3713 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3714
3715
phajdan.jre328cf92016-08-22 04:12:17 -07003716def write_json(path, contents):
3717 with open(path, 'w') as f:
3718 json.dump(contents, f)
3719
3720
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003721@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003722def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003723 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003724
3725 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003726 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003727 parser.add_option('-r', '--reverse', action='store_true',
3728 help='Lookup the branch(es) for the specified issues. If '
3729 'no issues are specified, all branches with mapped '
3730 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003731 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003732 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003733 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003734 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735
dnj@chromium.org406c4402015-03-03 17:22:28 +00003736 if options.reverse:
3737 branches = RunGit(['for-each-ref', 'refs/heads',
3738 '--format=%(refname:short)']).splitlines()
3739
3740 # Reverse issue lookup.
3741 issue_branch_map = {}
3742 for branch in branches:
3743 cl = Changelist(branchref=branch)
3744 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3745 if not args:
3746 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003747 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003748 for issue in args:
3749 if not issue:
3750 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003751 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print('Branch for issue number %s: %s' % (
3753 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003754 if options.json:
3755 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003756 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003757 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003758 if len(args) > 0:
3759 try:
3760 issue = int(args[0])
3761 except ValueError:
3762 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003763 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003764 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003765 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003766 if options.json:
3767 write_json(options.json, {
3768 'issue': cl.GetIssue(),
3769 'issue_url': cl.GetIssueURL(),
3770 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771 return 0
3772
3773
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003774def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003775 """Shows or posts review comments for any changelist."""
3776 parser.add_option('-a', '--add-comment', dest='comment',
3777 help='comment to add to an issue')
3778 parser.add_option('-i', dest='issue',
3779 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003780 parser.add_option('-j', '--json-file',
3781 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003782 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003783 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003784 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003785
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003786 issue = None
3787 if options.issue:
3788 try:
3789 issue = int(options.issue)
3790 except ValueError:
3791 DieWithError('A review issue id is expected to be a number')
3792
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003793 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003794
3795 if options.comment:
3796 cl.AddComment(options.comment)
3797 return 0
3798
3799 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003800 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003801 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003802 summary.append({
3803 'date': message['date'],
3804 'lgtm': False,
3805 'message': message['text'],
3806 'not_lgtm': False,
3807 'sender': message['sender'],
3808 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003809 if message['disapproval']:
3810 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003811 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003812 elif message['approval']:
3813 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003814 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003815 elif message['sender'] == data['owner_email']:
3816 color = Fore.MAGENTA
3817 else:
3818 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003819 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003820 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003821 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003822 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003823 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003824 if options.json_file:
3825 with open(options.json_file, 'wb') as f:
3826 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003827 return 0
3828
3829
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003830@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003831def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003832 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003833 parser.add_option('-d', '--display', action='store_true',
3834 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003835 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003836 help='New description to set for this issue (- for stdin, '
3837 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003838 parser.add_option('-f', '--force', action='store_true',
3839 help='Delete any unpublished Gerrit edits for this issue '
3840 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003841
3842 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003843 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003844 options, args = parser.parse_args(args)
3845 _process_codereview_select_options(parser, options)
3846
3847 target_issue = None
3848 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003849 target_issue = ParseIssueNumberArgument(args[0])
3850 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003851 parser.print_help()
3852 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003853
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003854 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003855
martiniss6eda05f2016-06-30 10:18:35 -07003856 kwargs = {
3857 'auth_config': auth_config,
3858 'codereview': options.forced_codereview,
3859 }
3860 if target_issue:
3861 kwargs['issue'] = target_issue.issue
3862 if options.forced_codereview == 'rietveld':
3863 kwargs['rietveld_server'] = target_issue.hostname
3864
3865 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003866
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003867 if not cl.GetIssue():
3868 DieWithError('This branch has no associated changelist.')
3869 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003870
smut@google.com34fb6b12015-07-13 20:03:26 +00003871 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003872 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003873 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003874
3875 if options.new_description:
3876 text = options.new_description
3877 if text == '-':
3878 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003879 elif text == '+':
3880 base_branch = cl.GetCommonAncestorWithUpstream()
3881 change = cl.GetChange(base_branch, None, local_description=True)
3882 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003883
3884 description.set_description(text)
3885 else:
3886 description.prompt()
3887
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003888 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003889 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003890 return 0
3891
3892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893def CreateDescriptionFromLog(args):
3894 """Pulls out the commit log to use as a base for the CL description."""
3895 log_args = []
3896 if len(args) == 1 and not args[0].endswith('.'):
3897 log_args = [args[0] + '..']
3898 elif len(args) == 1 and args[0].endswith('...'):
3899 log_args = [args[0][:-1]]
3900 elif len(args) == 2:
3901 log_args = [args[0] + '..' + args[1]]
3902 else:
3903 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003904 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905
3906
thestig@chromium.org44202a22014-03-11 19:22:18 +00003907def CMDlint(parser, args):
3908 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003909 parser.add_option('--filter', action='append', metavar='-x,+y',
3910 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003911 auth.add_auth_options(parser)
3912 options, args = parser.parse_args(args)
3913 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003914
3915 # Access to a protected member _XX of a client class
3916 # pylint: disable=W0212
3917 try:
3918 import cpplint
3919 import cpplint_chromium
3920 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003921 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003922 return 1
3923
3924 # Change the current working directory before calling lint so that it
3925 # shows the correct base.
3926 previous_cwd = os.getcwd()
3927 os.chdir(settings.GetRoot())
3928 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003929 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003930 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3931 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003932 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003933 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003934 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003935
3936 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003937 command = args + files
3938 if options.filter:
3939 command = ['--filter=' + ','.join(options.filter)] + command
3940 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003941
3942 white_regex = re.compile(settings.GetLintRegex())
3943 black_regex = re.compile(settings.GetLintIgnoreRegex())
3944 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3945 for filename in filenames:
3946 if white_regex.match(filename):
3947 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003948 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003949 else:
3950 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3951 extra_check_functions)
3952 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003953 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003954 finally:
3955 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003956 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003957 if cpplint._cpplint_state.error_count != 0:
3958 return 1
3959 return 0
3960
3961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003962def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003963 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003964 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003965 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003966 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003967 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003968 auth.add_auth_options(parser)
3969 options, args = parser.parse_args(args)
3970 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971
sbc@chromium.org71437c02015-04-09 19:29:40 +00003972 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003973 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974 return 1
3975
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003976 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977 if args:
3978 base_branch = args[0]
3979 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003980 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003981 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003982
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003983 cl.RunHook(
3984 committing=not options.upload,
3985 may_prompt=False,
3986 verbose=options.verbose,
3987 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003988 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989
3990
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003991def GenerateGerritChangeId(message):
3992 """Returns Ixxxxxx...xxx change id.
3993
3994 Works the same way as
3995 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3996 but can be called on demand on all platforms.
3997
3998 The basic idea is to generate git hash of a state of the tree, original commit
3999 message, author/committer info and timestamps.
4000 """
4001 lines = []
4002 tree_hash = RunGitSilent(['write-tree'])
4003 lines.append('tree %s' % tree_hash.strip())
4004 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4005 if code == 0:
4006 lines.append('parent %s' % parent.strip())
4007 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4008 lines.append('author %s' % author.strip())
4009 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4010 lines.append('committer %s' % committer.strip())
4011 lines.append('')
4012 # Note: Gerrit's commit-hook actually cleans message of some lines and
4013 # whitespace. This code is not doing this, but it clearly won't decrease
4014 # entropy.
4015 lines.append(message)
4016 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4017 stdin='\n'.join(lines))
4018 return 'I%s' % change_hash.strip()
4019
4020
wittman@chromium.org455dc922015-01-26 20:15:50 +00004021def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4022 """Computes the remote branch ref to use for the CL.
4023
4024 Args:
4025 remote (str): The git remote for the CL.
4026 remote_branch (str): The git remote branch for the CL.
4027 target_branch (str): The target branch specified by the user.
4028 pending_prefix (str): The pending prefix from the settings.
4029 """
4030 if not (remote and remote_branch):
4031 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004032
wittman@chromium.org455dc922015-01-26 20:15:50 +00004033 if target_branch:
4034 # Cannonicalize branch references to the equivalent local full symbolic
4035 # refs, which are then translated into the remote full symbolic refs
4036 # below.
4037 if '/' not in target_branch:
4038 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4039 else:
4040 prefix_replacements = (
4041 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4042 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4043 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4044 )
4045 match = None
4046 for regex, replacement in prefix_replacements:
4047 match = re.search(regex, target_branch)
4048 if match:
4049 remote_branch = target_branch.replace(match.group(0), replacement)
4050 break
4051 if not match:
4052 # This is a branch path but not one we recognize; use as-is.
4053 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004054 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4055 # Handle the refs that need to land in different refs.
4056 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004057
wittman@chromium.org455dc922015-01-26 20:15:50 +00004058 # Create the true path to the remote branch.
4059 # Does the following translation:
4060 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4061 # * refs/remotes/origin/master -> refs/heads/master
4062 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4063 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4064 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4065 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4066 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4067 'refs/heads/')
4068 elif remote_branch.startswith('refs/remotes/branch-heads'):
4069 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4070 # If a pending prefix exists then replace refs/ with it.
4071 if pending_prefix:
4072 remote_branch = remote_branch.replace('refs/', pending_prefix)
4073 return remote_branch
4074
4075
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004076def cleanup_list(l):
4077 """Fixes a list so that comma separated items are put as individual items.
4078
4079 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4080 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4081 """
4082 items = sum((i.split(',') for i in l), [])
4083 stripped_items = (i.strip() for i in items)
4084 return sorted(filter(None, stripped_items))
4085
4086
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004087@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004088def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004089 """Uploads the current changelist to codereview.
4090
4091 Can skip dependency patchset uploads for a branch by running:
4092 git config branch.branch_name.skip-deps-uploads True
4093 To unset run:
4094 git config --unset branch.branch_name.skip-deps-uploads
4095 Can also set the above globally by using the --global flag.
4096 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004097 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4098 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004099 parser.add_option('--bypass-watchlists', action='store_true',
4100 dest='bypass_watchlists',
4101 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004102 parser.add_option('-f', action='store_true', dest='force',
4103 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004104 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004105 parser.add_option('-b', '--bug',
4106 help='pre-populate the bug number(s) for this issue. '
4107 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004108 parser.add_option('--message-file', dest='message_file',
4109 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004110 parser.add_option('-t', dest='title',
4111 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004112 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004113 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004114 help='reviewer email addresses')
4115 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004116 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004117 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004118 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004119 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004120 parser.add_option('--emulate_svn_auto_props',
4121 '--emulate-svn-auto-props',
4122 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004123 dest="emulate_svn_auto_props",
4124 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004125 parser.add_option('-c', '--use-commit-queue', action='store_true',
4126 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004127 parser.add_option('--private', action='store_true',
4128 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004129 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004130 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004131 metavar='TARGET',
4132 help='Apply CL to remote ref TARGET. ' +
4133 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004134 parser.add_option('--squash', action='store_true',
4135 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004136 parser.add_option('--no-squash', action='store_true',
4137 help='Don\'t squash multiple commits into one ' +
4138 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004139 parser.add_option('--topic', default=None,
4140 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004141 parser.add_option('--email', default=None,
4142 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004143 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4144 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004145 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4146 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004147 help='Send the patchset to do a CQ dry run right after '
4148 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004149 parser.add_option('--dependencies', action='store_true',
4150 help='Uploads CLs of all the local branches that depend on '
4151 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004152
rmistry@google.com2dd99862015-06-22 12:22:18 +00004153 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004154 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004155 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004156 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004157 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004158 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004159 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004160
sbc@chromium.org71437c02015-04-09 19:29:40 +00004161 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004162 return 1
4163
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004164 options.reviewers = cleanup_list(options.reviewers)
4165 options.cc = cleanup_list(options.cc)
4166
tandriib80458a2016-06-23 12:20:07 -07004167 if options.message_file:
4168 if options.message:
4169 parser.error('only one of --message and --message-file allowed.')
4170 options.message = gclient_utils.FileRead(options.message_file)
4171 options.message_file = None
4172
tandrii4d0545a2016-07-06 03:56:49 -07004173 if options.cq_dry_run and options.use_commit_queue:
4174 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4175
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004176 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4177 settings.GetIsGerrit()
4178
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004179 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004180 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004181
4182
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004183def IsSubmoduleMergeCommit(ref):
4184 # When submodules are added to the repo, we expect there to be a single
4185 # non-git-svn merge commit at remote HEAD with a signature comment.
4186 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004187 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004188 return RunGit(cmd) != ''
4189
4190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004191def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004192 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004193
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004194 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4195 upstream and closes the issue automatically and atomically.
4196
4197 Otherwise (in case of Rietveld):
4198 Squashes branch into a single commit.
4199 Updates changelog with metadata (e.g. pointer to review).
4200 Pushes/dcommits the code upstream.
4201 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202 """
4203 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4204 help='bypass upload presubmit hook')
4205 parser.add_option('-m', dest='message',
4206 help="override review description")
4207 parser.add_option('-f', action='store_true', dest='force',
4208 help="force yes to questions (don't prompt)")
4209 parser.add_option('-c', dest='contributor',
4210 help="external contributor for patch (appended to " +
4211 "description and used as author for git). Should be " +
4212 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004213 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004214 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004215 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004216 auth_config = auth.extract_auth_config_from_options(options)
4217
4218 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004220 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4221 if cl.IsGerrit():
4222 if options.message:
4223 # This could be implemented, but it requires sending a new patch to
4224 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4225 # Besides, Gerrit has the ability to change the commit message on submit
4226 # automatically, thus there is no need to support this option (so far?).
4227 parser.error('-m MESSAGE option is not supported for Gerrit.')
4228 if options.contributor:
4229 parser.error(
4230 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4231 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4232 'the contributor\'s "name <email>". If you can\'t upload such a '
4233 'commit for review, contact your repository admin and request'
4234 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004235 if not cl.GetIssue():
4236 DieWithError('You must upload the issue first to Gerrit.\n'
4237 ' If you would rather have `git cl land` upload '
4238 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004239 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4240 options.verbose)
4241
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004242 current = cl.GetBranch()
4243 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4244 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004245 print()
4246 print('Attempting to push branch %r into another local branch!' % current)
4247 print()
4248 print('Either reparent this branch on top of origin/master:')
4249 print(' git reparent-branch --root')
4250 print()
4251 print('OR run `git rebase-update` if you think the parent branch is ')
4252 print('already committed.')
4253 print()
4254 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004255 return 1
4256
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004257 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 # Default to merging against our best guess of the upstream branch.
4259 args = [cl.GetUpstreamBranch()]
4260
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004261 if options.contributor:
4262 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004263 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004264 return 1
4265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004267 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004268
sbc@chromium.org71437c02015-04-09 19:29:40 +00004269 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270 return 1
4271
4272 # This rev-list syntax means "show all commits not in my branch that
4273 # are in base_branch".
4274 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4275 base_branch]).splitlines()
4276 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004277 print('Base branch "%s" has %d commits '
4278 'not in this branch.' % (base_branch, len(upstream_commits)))
4279 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004280 return 1
4281
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004282 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004283 svn_head = None
4284 if cmd == 'dcommit' or base_has_submodules:
4285 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4286 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004288 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004289 # If the base_head is a submodule merge commit, the first parent of the
4290 # base_head should be a git-svn commit, which is what we're interested in.
4291 base_svn_head = base_branch
4292 if base_has_submodules:
4293 base_svn_head += '^1'
4294
4295 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print('This branch has %d additional commits not upstreamed yet.'
4298 % len(extra_commits.splitlines()))
4299 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4300 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301 return 1
4302
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004303 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004304 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004305 author = None
4306 if options.contributor:
4307 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004308 hook_results = cl.RunHook(
4309 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004310 may_prompt=not options.force,
4311 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004312 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004313 if not hook_results.should_continue():
4314 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004316 # Check the tree status if the tree status URL is set.
4317 status = GetTreeStatus()
4318 if 'closed' == status:
4319 print('The tree is closed. Please wait for it to reopen. Use '
4320 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4321 return 1
4322 elif 'unknown' == status:
4323 print('Unable to determine tree status. Please verify manually and '
4324 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4325 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004326
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004327 change_desc = ChangeDescription(options.message)
4328 if not change_desc.description and cl.GetIssue():
4329 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004331 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004332 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004333 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004334 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004335 print('No description set.')
4336 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004337 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004338
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004339 # Keep a separate copy for the commit message, because the commit message
4340 # contains the link to the Rietveld issue, while the Rietveld message contains
4341 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004342 # Keep a separate copy for the commit message.
4343 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004344 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004345
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004346 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004347 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004348 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004349 # after it. Add a period on a new line to circumvent this. Also add a space
4350 # before the period to make sure that Gitiles continues to correctly resolve
4351 # the URL.
4352 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004353 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004354 commit_desc.append_footer('Patch from %s.' % options.contributor)
4355
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004356 print('Description:')
4357 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004358
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004359 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004361 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004363 # We want to squash all this branch's commits into one commit with the proper
4364 # description. We do this by doing a "reset --soft" to the base branch (which
4365 # keeps the working copy the same), then dcommitting that. If origin/master
4366 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4367 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004368 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004369 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4370 # Delete the branches if they exist.
4371 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4372 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4373 result = RunGitWithCode(showref_cmd)
4374 if result[0] == 0:
4375 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376
4377 # We might be in a directory that's present in this branch but not in the
4378 # trunk. Move up to the top of the tree so that git commands that expect a
4379 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004380 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381 if rel_base_path:
4382 os.chdir(rel_base_path)
4383
4384 # Stuff our change into the merge branch.
4385 # We wrap in a try...finally block so if anything goes wrong,
4386 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004387 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004388 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004389 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004390 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004392 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004393 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004395 RunGit(
4396 [
4397 'commit', '--author', options.contributor,
4398 '-m', commit_desc.description,
4399 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004401 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004402 if base_has_submodules:
4403 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4404 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4405 RunGit(['checkout', CHERRY_PICK_BRANCH])
4406 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004407 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004408 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004409 mirror = settings.GetGitMirror(remote)
4410 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004411 pending_prefix = settings.GetPendingRefPrefix()
4412 if not pending_prefix or branch.startswith(pending_prefix):
4413 # If not using refs/pending/heads/* at all, or target ref is already set
4414 # to pending, then push to the target ref directly.
4415 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004416 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004417 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004418 else:
4419 # Cherry-pick the change on top of pending ref and then push it.
4420 assert branch.startswith('refs/'), branch
4421 assert pending_prefix[-1] == '/', pending_prefix
4422 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004423 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004424 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004425 if retcode == 0:
4426 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427 else:
4428 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004429 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004430 'svn', 'dcommit',
4431 '-C%s' % options.similarity,
4432 '--no-rebase', '--rmdir',
4433 ]
4434 if settings.GetForceHttpsCommitUrl():
4435 # Allow forcing https commit URLs for some projects that don't allow
4436 # committing to http URLs (like Google Code).
4437 remote_url = cl.GetGitSvnRemoteUrl()
4438 if urlparse.urlparse(remote_url).scheme == 'http':
4439 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004440 cmd_args.append('--commit-url=%s' % remote_url)
4441 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004442 if 'Committed r' in output:
4443 revision = re.match(
4444 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4445 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446 finally:
4447 # And then swap back to the original branch and clean up.
4448 RunGit(['checkout', '-q', cl.GetBranch()])
4449 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004450 if base_has_submodules:
4451 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004452
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004453 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004454 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004455 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004456
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004457 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004458 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004459 try:
4460 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4461 # We set pushed_to_pending to False, since it made it all the way to the
4462 # real ref.
4463 pushed_to_pending = False
4464 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004465 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004468 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004469 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004470 if not to_pending:
4471 if viewvc_url and revision:
4472 change_desc.append_footer(
4473 'Committed: %s%s' % (viewvc_url, revision))
4474 elif revision:
4475 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004476 print('Closing issue '
4477 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004478 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004480 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004481 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004482 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004483 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004484 if options.bypass_hooks:
4485 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4486 else:
4487 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004488 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004489
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004490 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004491 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004492 print('The commit is in the pending queue (%s).' % pending_ref)
4493 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4494 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004495
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004496 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4497 if os.path.isfile(hook):
4498 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004499
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004500 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004501
4502
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004503def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004504 print()
4505 print('Waiting for commit to be landed on %s...' % real_ref)
4506 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004507 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4508 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004509 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004510
4511 loop = 0
4512 while True:
4513 sys.stdout.write('fetching (%d)... \r' % loop)
4514 sys.stdout.flush()
4515 loop += 1
4516
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004517 if mirror:
4518 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004519 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4520 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4521 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4522 for commit in commits.splitlines():
4523 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004524 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004525 return commit
4526
4527 current_rev = to_rev
4528
4529
tandriibf429402016-09-14 07:09:12 -07004530def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004531 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4532
4533 Returns:
4534 (retcode of last operation, output log of last operation).
4535 """
4536 assert pending_ref.startswith('refs/'), pending_ref
4537 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4538 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4539 code = 0
4540 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004541 max_attempts = 3
4542 attempts_left = max_attempts
4543 while attempts_left:
4544 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004545 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004546 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004547
4548 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004549 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004550 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004551 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004552 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004553 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004554 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004555 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004556 continue
4557
4558 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004559 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004560 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004561 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004562 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004563 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4564 'the following files have merge conflicts:' % pending_ref)
4565 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4566 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004567 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004568 return code, out
4569
4570 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004571 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004572 code, out = RunGitWithCode(
4573 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4574 if code == 0:
4575 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004577 return code, out
4578
vapiera7fbd5a2016-06-16 09:17:49 -07004579 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004580 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004581 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004582 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004583 print('Fatal push error. Make sure your .netrc credentials and git '
4584 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004585 return code, out
4586
vapiera7fbd5a2016-06-16 09:17:49 -07004587 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004588 return code, out
4589
4590
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004591def IsFatalPushFailure(push_stdout):
4592 """True if retrying push won't help."""
4593 return '(prohibited by Gerrit)' in push_stdout
4594
4595
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004596@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004598 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004599 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004600 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004601 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004602 message = """This repository appears to be a git-svn mirror, but we
4603don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004604 else:
4605 message = """This doesn't appear to be an SVN repository.
4606If your project has a true, writeable git repository, you probably want to run
4607'git cl land' instead.
4608If your project has a git mirror of an upstream SVN master, you probably need
4609to run 'git svn init'.
4610
4611Using the wrong command might cause your commit to appear to succeed, and the
4612review to be closed, without actually landing upstream. If you choose to
4613proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004614 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004615 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004616 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4617 'Please let us know of this project you are committing to:'
4618 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004619 return SendUpstream(parser, args, 'dcommit')
4620
4621
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004622@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004623def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004624 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004625 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626 print('This appears to be an SVN repository.')
4627 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004628 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004629 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004630 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004631
4632
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004633@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004634def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004635 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004636 parser.add_option('-b', dest='newbranch',
4637 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004638 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004640 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4641 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004642 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004643 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004644 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004645 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004647 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004648
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004649
4650 group = optparse.OptionGroup(
4651 parser,
4652 'Options for continuing work on the current issue uploaded from a '
4653 'different clone (e.g. different machine). Must be used independently '
4654 'from the other options. No issue number should be specified, and the '
4655 'branch must have an issue number associated with it')
4656 group.add_option('--reapply', action='store_true', dest='reapply',
4657 help='Reset the branch and reapply the issue.\n'
4658 'CAUTION: This will undo any local changes in this '
4659 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004660
4661 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004662 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004663 parser.add_option_group(group)
4664
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004665 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004666 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004667 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004668 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004669 auth_config = auth.extract_auth_config_from_options(options)
4670
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004671
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004672 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004673 if options.newbranch:
4674 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004675 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004676 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004677
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004678 cl = Changelist(auth_config=auth_config,
4679 codereview=options.forced_codereview)
4680 if not cl.GetIssue():
4681 parser.error('current branch must have an associated issue')
4682
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004683 upstream = cl.GetUpstreamBranch()
4684 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004685 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004686
4687 RunGit(['reset', '--hard', upstream])
4688 if options.pull:
4689 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004690
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004691 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4692 options.directory)
4693
4694 if len(args) != 1 or not args[0]:
4695 parser.error('Must specify issue number or url')
4696
4697 # We don't want uncommitted changes mixed up with the patch.
4698 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004699 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004700
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004701 if options.newbranch:
4702 if options.force:
4703 RunGit(['branch', '-D', options.newbranch],
4704 stderr=subprocess2.PIPE, error_ok=True)
4705 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004706 elif not GetCurrentBranch():
4707 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004708
4709 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4710
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004711 if cl.IsGerrit():
4712 if options.reject:
4713 parser.error('--reject is not supported with Gerrit codereview.')
4714 if options.nocommit:
4715 parser.error('--nocommit is not supported with Gerrit codereview.')
4716 if options.directory:
4717 parser.error('--directory is not supported with Gerrit codereview.')
4718
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004719 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004720 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721
4722
4723def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004724 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004725 # Provide a wrapper for git svn rebase to help avoid accidental
4726 # git svn dcommit.
4727 # It's the only command that doesn't use parser at all since we just defer
4728 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004729
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004730 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004731
4732
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004733def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734 """Fetches the tree status and returns either 'open', 'closed',
4735 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004736 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004737 if url:
4738 status = urllib2.urlopen(url).read().lower()
4739 if status.find('closed') != -1 or status == '0':
4740 return 'closed'
4741 elif status.find('open') != -1 or status == '1':
4742 return 'open'
4743 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004744 return 'unset'
4745
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004747def GetTreeStatusReason():
4748 """Fetches the tree status from a json url and returns the message
4749 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004750 url = settings.GetTreeStatusUrl()
4751 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004752 connection = urllib2.urlopen(json_url)
4753 status = json.loads(connection.read())
4754 connection.close()
4755 return status['message']
4756
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004757
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004758def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004759 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004760 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004761 status = GetTreeStatus()
4762 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004763 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004764 return 2
4765
vapiera7fbd5a2016-06-16 09:17:49 -07004766 print('The tree is %s' % status)
4767 print()
4768 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004769 if status != 'open':
4770 return 1
4771 return 0
4772
4773
maruel@chromium.org15192402012-09-06 12:38:29 +00004774def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004775 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004776 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004777 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004778 '-b', '--bot', action='append',
4779 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4780 'times to specify multiple builders. ex: '
4781 '"-b win_rel -b win_layout". See '
4782 'the try server waterfall for the builders name and the tests '
4783 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004784 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004785 '-B', '--bucket', default='',
4786 help=('Buildbucket bucket to send the try requests.'))
4787 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004788 '-m', '--master', default='',
4789 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004790 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004791 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004792 help='Revision to use for the try job; default: the revision will '
4793 'be determined by the try recipe that builder runs, which usually '
4794 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004795 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004796 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004797 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004798 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004799 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004800 '--project',
4801 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004802 'in recipe to determine to which repository or directory to '
4803 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004804 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004805 '-p', '--property', dest='properties', action='append', default=[],
4806 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004807 'key2=value2 etc. The value will be treated as '
4808 'json if decodable, or as string otherwise. '
4809 'NOTE: using this may make your try job not usable for CQ, '
4810 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004811 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004812 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4813 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004814 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004815 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004816 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004817 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004818
machenbach@chromium.org45453142015-09-15 08:45:22 +00004819 # Make sure that all properties are prop=value pairs.
4820 bad_params = [x for x in options.properties if '=' not in x]
4821 if bad_params:
4822 parser.error('Got properties with missing "=": %s' % bad_params)
4823
maruel@chromium.org15192402012-09-06 12:38:29 +00004824 if args:
4825 parser.error('Unknown arguments: %s' % args)
4826
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004827 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004828 if not cl.GetIssue():
4829 parser.error('Need to upload first')
4830
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004831 if cl.IsGerrit():
4832 parser.error(
4833 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4834 'If your project has Commit Queue, dry run is a workaround:\n'
4835 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004836
tandriie113dfd2016-10-11 10:20:12 -07004837 error_message = cl.CannotTriggerTryJobReason()
4838 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004839 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004840
borenet6c0efe62016-10-19 08:13:29 -07004841 if options.bucket and options.master:
4842 parser.error('Only one of --bucket and --master may be used.')
4843
qyearsley1fdfcb62016-10-24 13:22:03 -07004844 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004845
qyearsleydd49f942016-10-28 11:57:22 -07004846 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4847 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004848 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004849 if options.verbose:
4850 print('git cl try with no bots now defaults to CQ Dry Run.')
4851 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004852
borenet6c0efe62016-10-19 08:13:29 -07004853 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004854 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004855 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004856 'of bot requires an initial job from a parent (usually a builder). '
4857 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004858 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004859 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004860
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004861 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004862 if patchset != cl.GetPatchset():
4863 print('Warning: Codereview server has newer patchsets (%s) than most '
4864 'recent upload from local checkout (%s). Did a previous upload '
4865 'fail?\n'
4866 'By default, git cl try uses the latest patchset from '
4867 'codereview, continuing to use patchset %s.\n' %
4868 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004869
tandrii568043b2016-10-11 07:49:18 -07004870 try:
borenet6c0efe62016-10-19 08:13:29 -07004871 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4872 patchset)
tandrii568043b2016-10-11 07:49:18 -07004873 except BuildbucketResponseException as ex:
4874 print('ERROR: %s' % ex)
4875 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004876 return 0
4877
4878
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004879def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004880 """Prints info about try jobs associated with current CL."""
4881 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004882 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004883 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004884 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004885 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004886 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004887 '--color', action='store_true', default=setup_color.IS_TTY,
4888 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004889 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004890 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4891 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004892 group.add_option(
4893 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004894 parser.add_option_group(group)
4895 auth.add_auth_options(parser)
4896 options, args = parser.parse_args(args)
4897 if args:
4898 parser.error('Unrecognized args: %s' % ' '.join(args))
4899
4900 auth_config = auth.extract_auth_config_from_options(options)
4901 cl = Changelist(auth_config=auth_config)
4902 if not cl.GetIssue():
4903 parser.error('Need to upload first')
4904
tandrii221ab252016-10-06 08:12:04 -07004905 patchset = options.patchset
4906 if not patchset:
4907 patchset = cl.GetMostRecentPatchset()
4908 if not patchset:
4909 parser.error('Codereview doesn\'t know about issue %s. '
4910 'No access to issue or wrong issue number?\n'
4911 'Either upload first, or pass --patchset explicitely' %
4912 cl.GetIssue())
4913
4914 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004915 print('Warning: Codereview server has newer patchsets (%s) than most '
4916 'recent upload from local checkout (%s). Did a previous upload '
4917 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004918 'By default, git cl try-results uses the latest patchset from '
4919 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004920 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004921 try:
tandrii221ab252016-10-06 08:12:04 -07004922 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004923 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004924 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004925 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004926 if options.json:
4927 write_try_results_json(options.json, jobs)
4928 else:
4929 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 return 0
4931
4932
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004933@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004934def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004935 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004936 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004937 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004938 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004939
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004940 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004941 if args:
4942 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004943 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004944 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004945 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004946 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004947
4948 # Clear configured merge-base, if there is one.
4949 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004950 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004951 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004952 return 0
4953
4954
thestig@chromium.org00858c82013-12-02 23:08:03 +00004955def CMDweb(parser, args):
4956 """Opens the current CL in the web browser."""
4957 _, args = parser.parse_args(args)
4958 if args:
4959 parser.error('Unrecognized args: %s' % ' '.join(args))
4960
4961 issue_url = Changelist().GetIssueURL()
4962 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004963 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004964 return 1
4965
4966 webbrowser.open(issue_url)
4967 return 0
4968
4969
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004970def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004971 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004972 parser.add_option('-d', '--dry-run', action='store_true',
4973 help='trigger in dry run mode')
4974 parser.add_option('-c', '--clear', action='store_true',
4975 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004976 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004977 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004978 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004979 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004980 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004981 if args:
4982 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004983 if options.dry_run and options.clear:
4984 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4985
iannuccie53c9352016-08-17 14:40:40 -07004986 cl = Changelist(auth_config=auth_config, issue=options.issue,
4987 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004988 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004989 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004990 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004991 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004992 state = _CQState.DRY_RUN
4993 else:
4994 state = _CQState.COMMIT
4995 if not cl.GetIssue():
4996 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004997 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004998 return 0
4999
5000
groby@chromium.org411034a2013-02-26 15:12:01 +00005001def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005002 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005003 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005004 auth.add_auth_options(parser)
5005 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005006 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005007 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005008 if args:
5009 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005010 cl = Changelist(auth_config=auth_config, issue=options.issue,
5011 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005012 # Ensure there actually is an issue to close.
5013 cl.GetDescription()
5014 cl.CloseIssue()
5015 return 0
5016
5017
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005018def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005019 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005020 parser.add_option(
5021 '--stat',
5022 action='store_true',
5023 dest='stat',
5024 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005025 auth.add_auth_options(parser)
5026 options, args = parser.parse_args(args)
5027 auth_config = auth.extract_auth_config_from_options(options)
5028 if args:
5029 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005030
5031 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005032 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005033 # Staged changes would be committed along with the patch from last
5034 # upload, hence counted toward the "last upload" side in the final
5035 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005036 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005037 return 1
5038
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005039 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005040 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005041 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005042 if not issue:
5043 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005044 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005045 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005046
5047 # Create a new branch based on the merge-base
5048 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005049 # Clear cached branch in cl object, to avoid overwriting original CL branch
5050 # properties.
5051 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005052 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005053 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005054 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005055 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005056 return rtn
5057
wychen@chromium.org06928532015-02-03 02:11:29 +00005058 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005059 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005060 cmd = ['git', 'diff']
5061 if options.stat:
5062 cmd.append('--stat')
5063 cmd.extend([TMP_BRANCH, branch, '--'])
5064 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005065 finally:
5066 RunGit(['checkout', '-q', branch])
5067 RunGit(['branch', '-D', TMP_BRANCH])
5068
5069 return 0
5070
5071
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005072def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005073 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005074 parser.add_option(
5075 '--no-color',
5076 action='store_true',
5077 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005078 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005079 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005080 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005081
5082 author = RunGit(['config', 'user.email']).strip() or None
5083
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005084 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005085
5086 if args:
5087 if len(args) > 1:
5088 parser.error('Unknown args')
5089 base_branch = args[0]
5090 else:
5091 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005092 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005093
5094 change = cl.GetChange(base_branch, None)
5095 return owners_finder.OwnersFinder(
5096 [f.LocalPath() for f in
5097 cl.GetChange(base_branch, None).AffectedFiles()],
5098 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005099 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005100 disable_color=options.no_color).run()
5101
5102
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005103def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005104 """Generates a diff command."""
5105 # Generate diff for the current branch's changes.
5106 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5107 upstream_commit, '--' ]
5108
5109 if args:
5110 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005111 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005112 diff_cmd.append(arg)
5113 else:
5114 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005115
5116 return diff_cmd
5117
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005118def MatchingFileType(file_name, extensions):
5119 """Returns true if the file name ends with one of the given extensions."""
5120 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005121
enne@chromium.org555cfe42014-01-29 18:21:39 +00005122@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005123def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005124 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005125 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005126 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005127 parser.add_option('--full', action='store_true',
5128 help='Reformat the full content of all touched files')
5129 parser.add_option('--dry-run', action='store_true',
5130 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005131 parser.add_option('--python', action='store_true',
5132 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005133 parser.add_option('--diff', action='store_true',
5134 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005135 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005136
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005137 # git diff generates paths against the root of the repository. Change
5138 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005139 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005140 if rel_base_path:
5141 os.chdir(rel_base_path)
5142
digit@chromium.org29e47272013-05-17 17:01:46 +00005143 # Grab the merge-base commit, i.e. the upstream commit of the current
5144 # branch when it was created or the last time it was rebased. This is
5145 # to cover the case where the user may have called "git fetch origin",
5146 # moving the origin branch to a newer commit, but hasn't rebased yet.
5147 upstream_commit = None
5148 cl = Changelist()
5149 upstream_branch = cl.GetUpstreamBranch()
5150 if upstream_branch:
5151 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5152 upstream_commit = upstream_commit.strip()
5153
5154 if not upstream_commit:
5155 DieWithError('Could not find base commit for this branch. '
5156 'Are you in detached state?')
5157
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005158 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5159 diff_output = RunGit(changed_files_cmd)
5160 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005161 # Filter out files deleted by this CL
5162 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005163
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005164 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5165 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5166 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005167 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005168
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005169 top_dir = os.path.normpath(
5170 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5171
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005172 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5173 # formatted. This is used to block during the presubmit.
5174 return_value = 0
5175
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005176 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005177 # Locate the clang-format binary in the checkout
5178 try:
5179 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005180 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005181 DieWithError(e)
5182
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005183 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005184 cmd = [clang_format_tool]
5185 if not opts.dry_run and not opts.diff:
5186 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005187 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005188 if opts.diff:
5189 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005190 else:
5191 env = os.environ.copy()
5192 env['PATH'] = str(os.path.dirname(clang_format_tool))
5193 try:
5194 script = clang_format.FindClangFormatScriptInChromiumTree(
5195 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005196 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005197 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005198
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005199 cmd = [sys.executable, script, '-p0']
5200 if not opts.dry_run and not opts.diff:
5201 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005202
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005203 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5204 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005205
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005206 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5207 if opts.diff:
5208 sys.stdout.write(stdout)
5209 if opts.dry_run and len(stdout) > 0:
5210 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005211
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005212 # Similar code to above, but using yapf on .py files rather than clang-format
5213 # on C/C++ files
5214 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005215 yapf_tool = gclient_utils.FindExecutable('yapf')
5216 if yapf_tool is None:
5217 DieWithError('yapf not found in PATH')
5218
5219 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005220 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005221 cmd = [yapf_tool]
5222 if not opts.dry_run and not opts.diff:
5223 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005224 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005225 if opts.diff:
5226 sys.stdout.write(stdout)
5227 else:
5228 # TODO(sbc): yapf --lines mode still has some issues.
5229 # https://github.com/google/yapf/issues/154
5230 DieWithError('--python currently only works with --full')
5231
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005232 # Dart's formatter does not have the nice property of only operating on
5233 # modified chunks, so hard code full.
5234 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005235 try:
5236 command = [dart_format.FindDartFmtToolInChromiumTree()]
5237 if not opts.dry_run and not opts.diff:
5238 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005239 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005240
ppi@chromium.org6593d932016-03-03 15:41:15 +00005241 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005242 if opts.dry_run and stdout:
5243 return_value = 2
5244 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005245 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5246 'found in this checkout. Files in other languages are still '
5247 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005248
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005249 # Format GN build files. Always run on full build files for canonical form.
5250 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005251 cmd = ['gn', 'format' ]
5252 if opts.dry_run or opts.diff:
5253 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005254 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005255 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5256 shell=sys.platform == 'win32',
5257 cwd=top_dir)
5258 if opts.dry_run and gn_ret == 2:
5259 return_value = 2 # Not formatted.
5260 elif opts.diff and gn_ret == 2:
5261 # TODO this should compute and print the actual diff.
5262 print("This change has GN build file diff for " + gn_diff_file)
5263 elif gn_ret != 0:
5264 # For non-dry run cases (and non-2 return values for dry-run), a
5265 # nonzero error code indicates a failure, probably because the file
5266 # doesn't parse.
5267 DieWithError("gn format failed on " + gn_diff_file +
5268 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005269
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005270 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005271
5272
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005273@subcommand.usage('<codereview url or issue id>')
5274def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005275 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005276 _, args = parser.parse_args(args)
5277
5278 if len(args) != 1:
5279 parser.print_help()
5280 return 1
5281
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005282 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005283 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005284 parser.print_help()
5285 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005286 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005287
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005288 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005289 output = RunGit(['config', '--local', '--get-regexp',
5290 r'branch\..*\.%s' % issueprefix],
5291 error_ok=True)
5292 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005293 if issue == target_issue:
5294 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005295
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005296 branches = []
5297 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005298 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005299 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005300 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005301 return 1
5302 if len(branches) == 1:
5303 RunGit(['checkout', branches[0]])
5304 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005305 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005306 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005307 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005308 which = raw_input('Choose by index: ')
5309 try:
5310 RunGit(['checkout', branches[int(which)]])
5311 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005312 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005313 return 1
5314
5315 return 0
5316
5317
maruel@chromium.org29404b52014-09-08 22:58:00 +00005318def CMDlol(parser, args):
5319 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005320 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005321 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5322 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5323 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005324 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005325 return 0
5326
5327
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005328class OptionParser(optparse.OptionParser):
5329 """Creates the option parse and add --verbose support."""
5330 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005331 optparse.OptionParser.__init__(
5332 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005333 self.add_option(
5334 '-v', '--verbose', action='count', default=0,
5335 help='Use 2 times for more debugging info')
5336
5337 def parse_args(self, args=None, values=None):
5338 options, args = optparse.OptionParser.parse_args(self, args, values)
5339 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5340 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5341 return options, args
5342
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005343
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005344def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005345 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005346 print('\nYour python version %s is unsupported, please upgrade.\n' %
5347 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005348 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005349
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005350 # Reload settings.
5351 global settings
5352 settings = Settings()
5353
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005354 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005355 dispatcher = subcommand.CommandDispatcher(__name__)
5356 try:
5357 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005358 except auth.AuthenticationError as e:
5359 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005360 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005361 if e.code != 500:
5362 raise
5363 DieWithError(
5364 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5365 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005366 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005367
5368
5369if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005370 # These affect sys.stdout so do it outside of main() to simplify mocks in
5371 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005372 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005373 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005374 try:
5375 sys.exit(main(sys.argv[1:]))
5376 except KeyboardInterrupt:
5377 sys.stderr.write('interrupted\n')
5378 sys.exit(1)