blob: 5a81169bf350b72a4de8415a7e9b1c2a859ff1a9 [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
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100231def _get_committer_timestamp(commit):
232 """Returns unix timestamp as integer of a committer in a commit.
233
234 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
235 """
236 # Git also stores timezone offset, but it only affects visual display,
237 # actual point in time is defined by this timestamp only.
238 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
239
240
241def _git_amend_head(message, committer_timestamp):
242 """Amends commit with new message and desired committer_timestamp.
243
244 Sets committer timezone to UTC.
245 """
246 env = os.environ.copy()
247 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
248 return RunGit(['commit', '--amend', '-m', message], env=env)
249
250
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000251def add_git_similarity(parser):
252 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700253 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254 help='Sets the percentage that a pair of files need to match in order to'
255 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000256 parser.add_option(
257 '--find-copies', action='store_true',
258 help='Allows git to look for copies.')
259 parser.add_option(
260 '--no-find-copies', action='store_false', dest='find_copies',
261 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 old_parser_args = parser.parse_args
264 def Parse(args):
265 options, args = old_parser_args(args)
266
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000267 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700268 options.similarity = _git_get_branch_config_value(
269 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000270 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000271 print('Note: Saving similarity of %d%% in git config.'
272 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700273 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000274
iannucci@chromium.org79540052012-10-19 23:15:26 +0000275 options.similarity = max(0, min(options.similarity, 100))
276
277 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700278 options.find_copies = _git_get_branch_config_value(
279 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000280 else:
tandrii5d48c322016-08-18 16:19:37 -0700281 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000282
283 print('Using %d%% similarity for rename/copy detection. '
284 'Override with --similarity.' % options.similarity)
285
286 return options, args
287 parser.parse_args = Parse
288
289
machenbach@chromium.org45453142015-09-15 08:45:22 +0000290def _get_properties_from_options(options):
291 properties = dict(x.split('=', 1) for x in options.properties)
292 for key, val in properties.iteritems():
293 try:
294 properties[key] = json.loads(val)
295 except ValueError:
296 pass # If a value couldn't be evaluated, treat it as a string.
297 return properties
298
299
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000300def _prefix_master(master):
301 """Convert user-specified master name to full master name.
302
303 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
304 name, while the developers always use shortened master name
305 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
306 function does the conversion for buildbucket migration.
307 """
borenet6c0efe62016-10-19 08:13:29 -0700308 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 return master
borenet6c0efe62016-10-19 08:13:29 -0700310 return '%s%s' % (MASTER_PREFIX, master)
311
312
313def _unprefix_master(bucket):
314 """Convert bucket name to shortened master name.
315
316 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
317 name, while the developers always use shortened master name
318 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
319 function does the conversion for buildbucket migration.
320 """
321 if bucket.startswith(MASTER_PREFIX):
322 return bucket[len(MASTER_PREFIX):]
323 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324
325
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000326def _buildbucket_retry(operation_name, http, *args, **kwargs):
327 """Retries requests to buildbucket service and returns parsed json content."""
328 try_count = 0
329 while True:
330 response, content = http.request(*args, **kwargs)
331 try:
332 content_json = json.loads(content)
333 except ValueError:
334 content_json = None
335
336 # Buildbucket could return an error even if status==200.
337 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000338 error = content_json.get('error')
339 if error.get('code') == 403:
340 raise BuildbucketResponseException(
341 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000342 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000343 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344 raise BuildbucketResponseException(msg)
345
346 if response.status == 200:
347 if not content_json:
348 raise BuildbucketResponseException(
349 'Buildbucket returns invalid json content: %s.\n'
350 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
351 content)
352 return content_json
353 if response.status < 500 or try_count >= 2:
354 raise httplib2.HttpLib2Error(content)
355
356 # status >= 500 means transient failures.
357 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700358 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 try_count += 1
360 assert False, 'unreachable'
361
362
qyearsley1fdfcb62016-10-24 13:22:03 -0700363def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700364 """Returns a dict mapping bucket names to builders and tests,
365 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700366 """
qyearsleydd49f942016-10-28 11:57:22 -0700367 # If no bots are listed, we try to get a set of builders and tests based
368 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700369 if not options.bot:
370 change = changelist.GetChange(
371 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700372 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700373 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700374 change=change,
375 changed_files=change.LocalPaths(),
376 repository_root=settings.GetRoot(),
377 default_presubmit=None,
378 project=None,
379 verbose=options.verbose,
380 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700381 if masters is None:
382 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100383 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700384
qyearsley1fdfcb62016-10-24 13:22:03 -0700385 if options.bucket:
386 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700387 if options.master:
388 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700389
qyearsleydd49f942016-10-28 11:57:22 -0700390 # If bots are listed but no master or bucket, then we need to find out
391 # the corresponding master for each bot.
392 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
393 if error_message:
394 option_parser.error(
395 'Tryserver master cannot be found because: %s\n'
396 'Please manually specify the tryserver master, e.g. '
397 '"-m tryserver.chromium.linux".' % error_message)
398 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700399
400
qyearsley123a4682016-10-26 09:12:17 -0700401def _get_bucket_map_for_builders(builders):
402 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700403 map_url = 'https://builders-map.appspot.com/'
404 try:
qyearsley123a4682016-10-26 09:12:17 -0700405 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700406 except urllib2.URLError as e:
407 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
408 (map_url, e))
409 except ValueError as e:
410 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700411 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700412 return None, 'Failed to build master map.'
413
qyearsley123a4682016-10-26 09:12:17 -0700414 bucket_map = {}
415 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700416 masters = builders_map.get(builder, [])
417 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700419 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700421 (builder, masters))
422 bucket = _prefix_master(masters[0])
423 bucket_map.setdefault(bucket, {})[builder] = []
424
425 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700426
427
borenet6c0efe62016-10-19 08:13:29 -0700428def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700429 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 """Sends a request to Buildbucket to trigger try jobs for a changelist.
431
432 Args:
433 auth_config: AuthConfig for Rietveld.
434 changelist: Changelist that the try jobs are associated with.
435 buckets: A nested dict mapping bucket names to builders to tests.
436 options: Command-line options.
437 """
tandriide281ae2016-10-12 06:02:30 -0700438 assert changelist.GetIssue(), 'CL must be uploaded first'
439 codereview_url = changelist.GetCodereviewServer()
440 assert codereview_url, 'CL must be uploaded first'
441 patchset = patchset or changelist.GetMostRecentPatchset()
442 assert patchset, 'CL must be uploaded first'
443
444 codereview_host = urlparse.urlparse(codereview_url).hostname
445 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000446 http = authenticator.authorize(httplib2.Http())
447 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700448
449 # TODO(tandrii): consider caching Gerrit CL details just like
450 # _RietveldChangelistImpl does, then caching values in these two variables
451 # won't be necessary.
452 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000453
454 buildbucket_put_url = (
455 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000456 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700457 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
458 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
459 hostname=codereview_host,
460 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000461 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700462
463 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
464 shared_parameters_properties['category'] = category
465 if options.clobber:
466 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700467 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700468 if extra_properties:
469 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000470
471 batch_req_body = {'builds': []}
472 print_text = []
473 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700474 for bucket, builders_and_tests in sorted(buckets.iteritems()):
475 print_text.append('Bucket: %s' % bucket)
476 master = None
477 if bucket.startswith(MASTER_PREFIX):
478 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000479 for builder, tests in sorted(builders_and_tests.iteritems()):
480 print_text.append(' %s: %s' % (builder, tests))
481 parameters = {
482 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000483 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700484 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000485 'revision': options.revision,
486 }],
tandrii8c5a3532016-11-04 07:52:02 -0700487 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000488 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000489 if 'presubmit' in builder.lower():
490 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000491 if tests:
492 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700493
494 tags = [
495 'builder:%s' % builder,
496 'buildset:%s' % buildset,
497 'user_agent:git_cl_try',
498 ]
499 if master:
500 parameters['properties']['master'] = master
501 tags.append('master:%s' % master)
502
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000503 batch_req_body['builds'].append(
504 {
505 'bucket': bucket,
506 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700508 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509 }
510 )
511
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700513 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 http,
515 buildbucket_put_url,
516 'PUT',
517 body=json.dumps(batch_req_body),
518 headers={'Content-Type': 'application/json'}
519 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000520 print_text.append('To see results here, run: git cl try-results')
521 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700522 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000523
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524
tandrii221ab252016-10-06 08:12:04 -0700525def fetch_try_jobs(auth_config, changelist, buildbucket_host,
526 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700527 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528
qyearsley53f48a12016-09-01 10:45:13 -0700529 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000530 """
tandrii221ab252016-10-06 08:12:04 -0700531 assert buildbucket_host
532 assert changelist.GetIssue(), 'CL must be uploaded first'
533 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
534 patchset = patchset or changelist.GetMostRecentPatchset()
535 assert patchset, 'CL must be uploaded first'
536
537 codereview_url = changelist.GetCodereviewServer()
538 codereview_host = urlparse.urlparse(codereview_url).hostname
539 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 if authenticator.has_cached_credentials():
541 http = authenticator.authorize(httplib2.Http())
542 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700543 print('Warning: Some results might be missing because %s' %
544 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700545 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 http = httplib2.Http()
547
548 http.force_exception_to_status_code = True
549
tandrii221ab252016-10-06 08:12:04 -0700550 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
551 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
552 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700554 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555 params = {'tag': 'buildset:%s' % buildset}
556
557 builds = {}
558 while True:
559 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700560 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700562 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 for build in content.get('builds', []):
564 builds[build['id']] = build
565 if 'next_cursor' in content:
566 params['start_cursor'] = content['next_cursor']
567 else:
568 break
569 return builds
570
571
qyearsleyeab3c042016-08-24 09:18:28 -0700572def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 """Prints nicely result of fetch_try_jobs."""
574 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700575 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 return
577
578 # Make a copy, because we'll be modifying builds dictionary.
579 builds = builds.copy()
580 builder_names_cache = {}
581
582 def get_builder(b):
583 try:
584 return builder_names_cache[b['id']]
585 except KeyError:
586 try:
587 parameters = json.loads(b['parameters_json'])
588 name = parameters['builder_name']
589 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700590 print('WARNING: failed to get builder name for build %s: %s' % (
591 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 name = None
593 builder_names_cache[b['id']] = name
594 return name
595
596 def get_bucket(b):
597 bucket = b['bucket']
598 if bucket.startswith('master.'):
599 return bucket[len('master.'):]
600 return bucket
601
602 if options.print_master:
603 name_fmt = '%%-%ds %%-%ds' % (
604 max(len(str(get_bucket(b))) for b in builds.itervalues()),
605 max(len(str(get_builder(b))) for b in builds.itervalues()))
606 def get_name(b):
607 return name_fmt % (get_bucket(b), get_builder(b))
608 else:
609 name_fmt = '%%-%ds' % (
610 max(len(str(get_builder(b))) for b in builds.itervalues()))
611 def get_name(b):
612 return name_fmt % get_builder(b)
613
614 def sort_key(b):
615 return b['status'], b.get('result'), get_name(b), b.get('url')
616
617 def pop(title, f, color=None, **kwargs):
618 """Pop matching builds from `builds` dict and print them."""
619
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000620 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000621 colorize = str
622 else:
623 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
624
625 result = []
626 for b in builds.values():
627 if all(b.get(k) == v for k, v in kwargs.iteritems()):
628 builds.pop(b['id'])
629 result.append(b)
630 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700631 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000632 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700633 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000634
635 total = len(builds)
636 pop(status='COMPLETED', result='SUCCESS',
637 title='Successes:', color=Fore.GREEN,
638 f=lambda b: (get_name(b), b.get('url')))
639 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
640 title='Infra Failures:', color=Fore.MAGENTA,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
643 title='Failures:', color=Fore.RED,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='CANCELED',
646 title='Canceled:', color=Fore.MAGENTA,
647 f=lambda b: (get_name(b),))
648 pop(status='COMPLETED', result='FAILURE',
649 failure_reason='INVALID_BUILD_DEFINITION',
650 title='Wrong master/builder name:', color=Fore.MAGENTA,
651 f=lambda b: (get_name(b),))
652 pop(status='COMPLETED', result='FAILURE',
653 title='Other failures:',
654 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
655 pop(status='COMPLETED',
656 title='Other finished:',
657 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
658 pop(status='STARTED',
659 title='Started:', color=Fore.YELLOW,
660 f=lambda b: (get_name(b), b.get('url')))
661 pop(status='SCHEDULED',
662 title='Scheduled:',
663 f=lambda b: (get_name(b), 'id=%s' % b['id']))
664 # The last section is just in case buildbucket API changes OR there is a bug.
665 pop(title='Other:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700668 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000669
670
qyearsley53f48a12016-09-01 10:45:13 -0700671def write_try_results_json(output_file, builds):
672 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
673
674 The input |builds| dict is assumed to be generated by Buildbucket.
675 Buildbucket documentation: http://goo.gl/G0s101
676 """
677
678 def convert_build_dict(build):
679 return {
680 'buildbucket_id': build.get('id'),
681 'status': build.get('status'),
682 'result': build.get('result'),
683 'bucket': build.get('bucket'),
684 'builder_name': json.loads(
685 build.get('parameters_json', '{}')).get('builder_name'),
686 'failure_reason': build.get('failure_reason'),
687 'url': build.get('url'),
688 }
689
690 converted = []
691 for _, build in sorted(builds.items()):
692 converted.append(convert_build_dict(build))
693 write_json(output_file, converted)
694
695
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000696def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
697 """Return the corresponding git ref if |base_url| together with |glob_spec|
698 matches the full |url|.
699
700 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
701 """
702 fetch_suburl, as_ref = glob_spec.split(':')
703 if allow_wildcards:
704 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
705 if glob_match:
706 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
707 # "branches/{472,597,648}/src:refs/remotes/svn/*".
708 branch_re = re.escape(base_url)
709 if glob_match.group(1):
710 branch_re += '/' + re.escape(glob_match.group(1))
711 wildcard = glob_match.group(2)
712 if wildcard == '*':
713 branch_re += '([^/]*)'
714 else:
715 # Escape and replace surrounding braces with parentheses and commas
716 # with pipe symbols.
717 wildcard = re.escape(wildcard)
718 wildcard = re.sub('^\\\\{', '(', wildcard)
719 wildcard = re.sub('\\\\,', '|', wildcard)
720 wildcard = re.sub('\\\\}$', ')', wildcard)
721 branch_re += wildcard
722 if glob_match.group(3):
723 branch_re += re.escape(glob_match.group(3))
724 match = re.match(branch_re, url)
725 if match:
726 return re.sub('\*$', match.group(1), as_ref)
727
728 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
729 if fetch_suburl:
730 full_url = base_url + '/' + fetch_suburl
731 else:
732 full_url = base_url
733 if full_url == url:
734 return as_ref
735 return None
736
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000737
iannucci@chromium.org79540052012-10-19 23:15:26 +0000738def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000739 """Prints statistics about the change to the user."""
740 # --no-ext-diff is broken in some versions of Git, so try to work around
741 # this by overriding the environment (but there is still a problem if the
742 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000743 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000744 if 'GIT_EXTERNAL_DIFF' in env:
745 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000746
747 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800748 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000749 else:
750 similarity_options = ['-M%s' % similarity]
751
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000752 try:
753 stdout = sys.stdout.fileno()
754 except AttributeError:
755 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000756 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000757 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000758 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000759 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000760
761
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000762class BuildbucketResponseException(Exception):
763 pass
764
765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766class Settings(object):
767 def __init__(self):
768 self.default_server = None
769 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000770 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771 self.is_git_svn = None
772 self.svn_branch = None
773 self.tree_status_url = None
774 self.viewvc_url = None
775 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000776 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000777 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000778 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000779 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000780 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000781 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000782 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
784 def LazyUpdateIfNeeded(self):
785 """Updates the settings from a codereview.settings file, if available."""
786 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000787 # The only value that actually changes the behavior is
788 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000789 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000790 error_ok=True
791 ).strip().lower()
792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000794 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 LoadCodereviewSettingsFromFile(cr_settings_file)
796 self.updated = True
797
798 def GetDefaultServerUrl(self, error_ok=False):
799 if not self.default_server:
800 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000801 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000802 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 if error_ok:
804 return self.default_server
805 if not self.default_server:
806 error_message = ('Could not find settings file. You must configure '
807 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000808 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 return self.default_server
811
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000812 @staticmethod
813 def GetRelativeRoot():
814 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000817 if self.root is None:
818 self.root = os.path.abspath(self.GetRelativeRoot())
819 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000821 def GetGitMirror(self, remote='origin'):
822 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000823 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000824 if not os.path.isdir(local_url):
825 return None
826 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
827 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
828 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
829 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
830 if mirror.exists():
831 return mirror
832 return None
833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 def GetIsGitSvn(self):
835 """Return true if this repo looks like it's using git-svn."""
836 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000837 if self.GetPendingRefPrefix():
838 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
839 self.is_git_svn = False
840 else:
841 # If you have any "svn-remote.*" config keys, we think you're using svn.
842 self.is_git_svn = RunGitWithCode(
843 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844 return self.is_git_svn
845
846 def GetSVNBranch(self):
847 if self.svn_branch is None:
848 if not self.GetIsGitSvn():
849 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
850
851 # Try to figure out which remote branch we're based on.
852 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000853 # 1) iterate through our branch history and find the svn URL.
854 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855
856 # regexp matching the git-svn line that contains the URL.
857 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
858
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000859 # We don't want to go through all of history, so read a line from the
860 # pipe at a time.
861 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000862 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000863 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
864 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000865 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000866 for line in proc.stdout:
867 match = git_svn_re.match(line)
868 if match:
869 url = match.group(1)
870 proc.stdout.close() # Cut pipe.
871 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000873 if url:
874 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
875 remotes = RunGit(['config', '--get-regexp',
876 r'^svn-remote\..*\.url']).splitlines()
877 for remote in remotes:
878 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000880 remote = match.group(1)
881 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000882 rewrite_root = RunGit(
883 ['config', 'svn-remote.%s.rewriteRoot' % remote],
884 error_ok=True).strip()
885 if rewrite_root:
886 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000887 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000888 ['config', 'svn-remote.%s.fetch' % remote],
889 error_ok=True).strip()
890 if fetch_spec:
891 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
892 if self.svn_branch:
893 break
894 branch_spec = RunGit(
895 ['config', 'svn-remote.%s.branches' % remote],
896 error_ok=True).strip()
897 if branch_spec:
898 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
899 if self.svn_branch:
900 break
901 tag_spec = RunGit(
902 ['config', 'svn-remote.%s.tags' % remote],
903 error_ok=True).strip()
904 if tag_spec:
905 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
906 if self.svn_branch:
907 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908
909 if not self.svn_branch:
910 DieWithError('Can\'t guess svn branch -- try specifying it on the '
911 'command line')
912
913 return self.svn_branch
914
915 def GetTreeStatusUrl(self, error_ok=False):
916 if not self.tree_status_url:
917 error_message = ('You must configure your tree status URL by running '
918 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000919 self.tree_status_url = self._GetRietveldConfig(
920 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921 return self.tree_status_url
922
923 def GetViewVCUrl(self):
924 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000925 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 return self.viewvc_url
927
rmistry@google.com90752582014-01-14 21:04:50 +0000928 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000929 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000930
rmistry@google.com78948ed2015-07-08 23:09:57 +0000931 def GetIsSkipDependencyUpload(self, branch_name):
932 """Returns true if specified branch should skip dep uploads."""
933 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
934 error_ok=True)
935
rmistry@google.com5626a922015-02-26 14:03:30 +0000936 def GetRunPostUploadHook(self):
937 run_post_upload_hook = self._GetRietveldConfig(
938 'run-post-upload-hook', error_ok=True)
939 return run_post_upload_hook == "True"
940
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000941 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000942 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000943
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000944 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000945 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000946
ukai@chromium.orge8077812012-02-03 03:41:46 +0000947 def GetIsGerrit(self):
948 """Return true if this repo is assosiated with gerrit code review system."""
949 if self.is_gerrit is None:
950 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
951 return self.is_gerrit
952
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000953 def GetSquashGerritUploads(self):
954 """Return true if uploads to Gerrit should be squashed by default."""
955 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700956 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
957 if self.squash_gerrit_uploads is None:
958 # Default is squash now (http://crbug.com/611892#c23).
959 self.squash_gerrit_uploads = not (
960 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
961 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000962 return self.squash_gerrit_uploads
963
tandriia60502f2016-06-20 02:01:53 -0700964 def GetSquashGerritUploadsOverride(self):
965 """Return True or False if codereview.settings should be overridden.
966
967 Returns None if no override has been defined.
968 """
969 # See also http://crbug.com/611892#c23
970 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
971 error_ok=True).strip()
972 if result == 'true':
973 return True
974 if result == 'false':
975 return False
976 return None
977
tandrii@chromium.org28253532016-04-14 13:46:56 +0000978 def GetGerritSkipEnsureAuthenticated(self):
979 """Return True if EnsureAuthenticated should not be done for Gerrit
980 uploads."""
981 if self.gerrit_skip_ensure_authenticated is None:
982 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000983 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000984 error_ok=True).strip() == 'true')
985 return self.gerrit_skip_ensure_authenticated
986
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000987 def GetGitEditor(self):
988 """Return the editor specified in the git config, or None if none is."""
989 if self.git_editor is None:
990 self.git_editor = self._GetConfig('core.editor', error_ok=True)
991 return self.git_editor or None
992
thestig@chromium.org44202a22014-03-11 19:22:18 +0000993 def GetLintRegex(self):
994 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
995 DEFAULT_LINT_REGEX)
996
997 def GetLintIgnoreRegex(self):
998 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
999 DEFAULT_LINT_IGNORE_REGEX)
1000
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001001 def GetProject(self):
1002 if not self.project:
1003 self.project = self._GetRietveldConfig('project', error_ok=True)
1004 return self.project
1005
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001006 def GetForceHttpsCommitUrl(self):
1007 if not self.force_https_commit_url:
1008 self.force_https_commit_url = self._GetRietveldConfig(
1009 'force-https-commit-url', error_ok=True)
1010 return self.force_https_commit_url
1011
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001012 def GetPendingRefPrefix(self):
1013 if not self.pending_ref_prefix:
1014 self.pending_ref_prefix = self._GetRietveldConfig(
1015 'pending-ref-prefix', error_ok=True)
1016 return self.pending_ref_prefix
1017
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001018 def _GetRietveldConfig(self, param, **kwargs):
1019 return self._GetConfig('rietveld.' + param, **kwargs)
1020
rmistry@google.com78948ed2015-07-08 23:09:57 +00001021 def _GetBranchConfig(self, branch_name, param, **kwargs):
1022 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 def _GetConfig(self, param, **kwargs):
1025 self.LazyUpdateIfNeeded()
1026 return RunGit(['config', param], **kwargs).strip()
1027
1028
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001029def ShouldGenerateGitNumberFooters():
1030 """Decides depending on codereview.settings file in the current checkout HEAD.
1031 """
1032 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1033 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1034 cr_settings_file = FindCodereviewSettingsFile()
1035 if not cr_settings_file:
1036 return False
1037 keyvals = gclient_utils.ParseCodereviewSettingsContent(
1038 cr_settings_file.read())
Andrii Shyshkalovb8c535f2016-11-24 18:01:52 +01001039 return keyvals.get('GENERATE_GIT_NUMBER_FOOTERS', '').lower() == 'true'
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001040
1041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042def ShortBranchName(branch):
1043 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001044 return branch.replace('refs/heads/', '', 1)
1045
1046
1047def GetCurrentBranchRef():
1048 """Returns branch ref (e.g., refs/heads/master) or None."""
1049 return RunGit(['symbolic-ref', 'HEAD'],
1050 stderr=subprocess2.VOID, error_ok=True).strip() or None
1051
1052
1053def GetCurrentBranch():
1054 """Returns current branch or None.
1055
1056 For refs/heads/* branches, returns just last part. For others, full ref.
1057 """
1058 branchref = GetCurrentBranchRef()
1059 if branchref:
1060 return ShortBranchName(branchref)
1061 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062
1063
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001064class _CQState(object):
1065 """Enum for states of CL with respect to Commit Queue."""
1066 NONE = 'none'
1067 DRY_RUN = 'dry_run'
1068 COMMIT = 'commit'
1069
1070 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1071
1072
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001073class _ParsedIssueNumberArgument(object):
1074 def __init__(self, issue=None, patchset=None, hostname=None):
1075 self.issue = issue
1076 self.patchset = patchset
1077 self.hostname = hostname
1078
1079 @property
1080 def valid(self):
1081 return self.issue is not None
1082
1083
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001084def ParseIssueNumberArgument(arg):
1085 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1086 fail_result = _ParsedIssueNumberArgument()
1087
1088 if arg.isdigit():
1089 return _ParsedIssueNumberArgument(issue=int(arg))
1090 if not arg.startswith('http'):
1091 return fail_result
1092 url = gclient_utils.UpgradeToHttps(arg)
1093 try:
1094 parsed_url = urlparse.urlparse(url)
1095 except ValueError:
1096 return fail_result
1097 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1098 tmp = cls.ParseIssueURL(parsed_url)
1099 if tmp is not None:
1100 return tmp
1101 return fail_result
1102
1103
Aaron Gablea45ee112016-11-22 15:14:38 -08001104class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001105 def __init__(self, issue, url):
1106 self.issue = issue
1107 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001108 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001109
1110 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001111 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001112 self.issue, self.url)
1113
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 """Changelist works with one changelist in local branch.
1117
1118 Supports two codereview backends: Rietveld or Gerrit, selected at object
1119 creation.
1120
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001121 Notes:
1122 * Not safe for concurrent multi-{thread,process} use.
1123 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001124 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001125 """
1126
1127 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1128 """Create a new ChangeList instance.
1129
1130 If issue is given, the codereview must be given too.
1131
1132 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1133 Otherwise, it's decided based on current configuration of the local branch,
1134 with default being 'rietveld' for backwards compatibility.
1135 See _load_codereview_impl for more details.
1136
1137 **kwargs will be passed directly to codereview implementation.
1138 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001140 global settings
1141 if not settings:
1142 # Happens when git_cl.py is used as a utility library.
1143 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144
1145 if issue:
1146 assert codereview, 'codereview must be known, if issue is known'
1147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 self.branchref = branchref
1149 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001150 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.branch = ShortBranchName(self.branchref)
1152 else:
1153 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001155 self.lookedup_issue = False
1156 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 self.has_description = False
1158 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001159 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001161 self.cc = None
1162 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001163 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001164
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001165 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001166 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001168 assert self._codereview_impl
1169 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170
1171 def _load_codereview_impl(self, codereview=None, **kwargs):
1172 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001173 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1174 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1175 self._codereview = codereview
1176 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001177 return
1178
1179 # Automatic selection based on issue number set for a current branch.
1180 # Rietveld takes precedence over Gerrit.
1181 assert not self.issue
1182 # Whether we find issue or not, we are doing the lookup.
1183 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001184 if self.GetBranch():
1185 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1186 issue = _git_get_branch_config_value(
1187 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1188 if issue:
1189 self._codereview = codereview
1190 self._codereview_impl = cls(self, **kwargs)
1191 self.issue = int(issue)
1192 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001193
1194 # No issue is set for this branch, so decide based on repo-wide settings.
1195 return self._load_codereview_impl(
1196 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1197 **kwargs)
1198
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001199 def IsGerrit(self):
1200 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001201
1202 def GetCCList(self):
1203 """Return the users cc'd on this CL.
1204
agable92bec4f2016-08-24 09:27:27 -07001205 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001206 """
1207 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001208 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001209 more_cc = ','.join(self.watchers)
1210 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1211 return self.cc
1212
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001213 def GetCCListWithoutDefault(self):
1214 """Return the users cc'd on this CL excluding default ones."""
1215 if self.cc is None:
1216 self.cc = ','.join(self.watchers)
1217 return self.cc
1218
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001219 def SetWatchers(self, watchers):
1220 """Set the list of email addresses that should be cc'd based on the changed
1221 files in this CL.
1222 """
1223 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224
1225 def GetBranch(self):
1226 """Returns the short branch name, e.g. 'master'."""
1227 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001229 if not branchref:
1230 return None
1231 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 self.branch = ShortBranchName(self.branchref)
1233 return self.branch
1234
1235 def GetBranchRef(self):
1236 """Returns the full branch name, e.g. 'refs/heads/master'."""
1237 self.GetBranch() # Poke the lazy loader.
1238 return self.branchref
1239
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001240 def ClearBranch(self):
1241 """Clears cached branch data of this object."""
1242 self.branch = self.branchref = None
1243
tandrii5d48c322016-08-18 16:19:37 -07001244 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1245 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1246 kwargs['branch'] = self.GetBranch()
1247 return _git_get_branch_config_value(key, default, **kwargs)
1248
1249 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1250 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1251 assert self.GetBranch(), (
1252 'this CL must have an associated branch to %sset %s%s' %
1253 ('un' if value is None else '',
1254 key,
1255 '' if value is None else ' to %r' % value))
1256 kwargs['branch'] = self.GetBranch()
1257 return _git_set_branch_config_value(key, value, **kwargs)
1258
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001259 @staticmethod
1260 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001261 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 e.g. 'origin', 'refs/heads/master'
1263 """
1264 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001265 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001268 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001270 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1271 error_ok=True).strip()
1272 if upstream_branch:
1273 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001275 # Fall back on trying a git-svn upstream branch.
1276 if settings.GetIsGitSvn():
1277 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001279 # Else, try to guess the origin remote.
1280 remote_branches = RunGit(['branch', '-r']).split()
1281 if 'origin/master' in remote_branches:
1282 # Fall back on origin/master if it exits.
1283 remote = 'origin'
1284 upstream_branch = 'refs/heads/master'
1285 elif 'origin/trunk' in remote_branches:
1286 # Fall back on origin/trunk if it exists. Generally a shared
1287 # git-svn clone
1288 remote = 'origin'
1289 upstream_branch = 'refs/heads/trunk'
1290 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001291 DieWithError(
1292 'Unable to determine default branch to diff against.\n'
1293 'Either pass complete "git diff"-style arguments, like\n'
1294 ' git cl upload origin/master\n'
1295 'or verify this branch is set up to track another \n'
1296 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297
1298 return remote, upstream_branch
1299
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001300 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001301 upstream_branch = self.GetUpstreamBranch()
1302 if not BranchExists(upstream_branch):
1303 DieWithError('The upstream for the current branch (%s) does not exist '
1304 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001305 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001306 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001307
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 def GetUpstreamBranch(self):
1309 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001310 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001312 upstream_branch = upstream_branch.replace('refs/heads/',
1313 'refs/remotes/%s/' % remote)
1314 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1315 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 self.upstream_branch = upstream_branch
1317 return self.upstream_branch
1318
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001319 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001320 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 remote, branch = None, self.GetBranch()
1322 seen_branches = set()
1323 while branch not in seen_branches:
1324 seen_branches.add(branch)
1325 remote, branch = self.FetchUpstreamTuple(branch)
1326 branch = ShortBranchName(branch)
1327 if remote != '.' or branch.startswith('refs/remotes'):
1328 break
1329 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001330 remotes = RunGit(['remote'], error_ok=True).split()
1331 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001332 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001333 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001334 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001335 logging.warning('Could not determine which remote this change is '
1336 'associated with, so defaulting to "%s". This may '
1337 'not be what you want. You may prevent this message '
1338 'by running "git svn info" as documented here: %s',
1339 self._remote,
1340 GIT_INSTRUCTIONS_URL)
1341 else:
1342 logging.warn('Could not determine which remote this change is '
1343 'associated with. You may prevent this message by '
1344 'running "git svn info" as documented here: %s',
1345 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 branch = 'HEAD'
1347 if branch.startswith('refs/remotes'):
1348 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001349 elif branch.startswith('refs/branch-heads/'):
1350 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 else:
1352 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001353 return self._remote
1354
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001355 def GitSanityChecks(self, upstream_git_obj):
1356 """Checks git repo status and ensures diff is from local commits."""
1357
sbc@chromium.org79706062015-01-14 21:18:12 +00001358 if upstream_git_obj is None:
1359 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001360 print('ERROR: unable to determine current branch (detached HEAD?)',
1361 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001362 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001363 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001364 return False
1365
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001366 # Verify the commit we're diffing against is in our current branch.
1367 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1368 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1369 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001370 print('ERROR: %s is not in the current branch. You may need to rebase '
1371 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001372 return False
1373
1374 # List the commits inside the diff, and verify they are all local.
1375 commits_in_diff = RunGit(
1376 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1377 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1378 remote_branch = remote_branch.strip()
1379 if code != 0:
1380 _, remote_branch = self.GetRemoteBranch()
1381
1382 commits_in_remote = RunGit(
1383 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1384
1385 common_commits = set(commits_in_diff) & set(commits_in_remote)
1386 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001387 print('ERROR: Your diff contains %d commits already in %s.\n'
1388 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1389 'the diff. If you are using a custom git flow, you can override'
1390 ' the reference used for this check with "git config '
1391 'gitcl.remotebranch <git-ref>".' % (
1392 len(common_commits), remote_branch, upstream_git_obj),
1393 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001394 return False
1395 return True
1396
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001397 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001398 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001399
1400 Returns None if it is not set.
1401 """
tandrii5d48c322016-08-18 16:19:37 -07001402 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001403
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001404 def GetGitSvnRemoteUrl(self):
1405 """Return the configured git-svn remote URL parsed from git svn info.
1406
1407 Returns None if it is not set.
1408 """
1409 # URL is dependent on the current directory.
1410 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1411 if data:
1412 keys = dict(line.split(': ', 1) for line in data.splitlines()
1413 if ': ' in line)
1414 return keys.get('URL', None)
1415 return None
1416
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 def GetRemoteUrl(self):
1418 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1419
1420 Returns None if there is no remote.
1421 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001422 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001423 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1424
1425 # If URL is pointing to a local directory, it is probably a git cache.
1426 if os.path.isdir(url):
1427 url = RunGit(['config', 'remote.%s.url' % remote],
1428 error_ok=True,
1429 cwd=url).strip()
1430 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001432 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001433 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001434 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001435 self.issue = self._GitGetBranchConfigValue(
1436 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 return self.issue
1439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 def GetIssueURL(self):
1441 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001442 issue = self.GetIssue()
1443 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001444 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001445 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446
1447 def GetDescription(self, pretty=False):
1448 if not self.has_description:
1449 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001450 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 self.has_description = True
1452 if pretty:
1453 wrapper = textwrap.TextWrapper()
1454 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1455 return wrapper.fill(self.description)
1456 return self.description
1457
1458 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001459 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001460 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001461 self.patchset = self._GitGetBranchConfigValue(
1462 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464 return self.patchset
1465
1466 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001467 """Set this branch's patchset. If patchset=0, clears the patchset."""
1468 assert self.GetBranch()
1469 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001470 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001471 else:
1472 self.patchset = int(patchset)
1473 self._GitSetBranchConfigValue(
1474 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001476 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001477 """Set this branch's issue. If issue isn't given, clears the issue."""
1478 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001480 issue = int(issue)
1481 self._GitSetBranchConfigValue(
1482 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001483 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001484 codereview_server = self._codereview_impl.GetCodereviewServer()
1485 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001486 self._GitSetBranchConfigValue(
1487 self._codereview_impl.CodereviewServerConfigKey(),
1488 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 else:
tandrii5d48c322016-08-18 16:19:37 -07001490 # Reset all of these just to be clean.
1491 reset_suffixes = [
1492 'last-upload-hash',
1493 self._codereview_impl.IssueConfigKey(),
1494 self._codereview_impl.PatchsetConfigKey(),
1495 self._codereview_impl.CodereviewServerConfigKey(),
1496 ] + self._PostUnsetIssueProperties()
1497 for prop in reset_suffixes:
1498 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001499 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001500 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501
dnjba1b0f32016-09-02 12:37:42 -07001502 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001503 if not self.GitSanityChecks(upstream_branch):
1504 DieWithError('\nGit sanity check failure')
1505
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001506 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001507 if not root:
1508 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001509 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001510
1511 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001513 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001514 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001515 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001516 except subprocess2.CalledProcessError:
1517 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001518 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001519 'This branch probably doesn\'t exist anymore. To reset the\n'
1520 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001521 ' git branch --set-upstream-to origin/master %s\n'
1522 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001523 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524
maruel@chromium.org52424302012-08-29 15:14:30 +00001525 issue = self.GetIssue()
1526 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001527 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001528 description = self.GetDescription()
1529 else:
1530 # If the change was never uploaded, use the log messages of all commits
1531 # up to the branch point, as git cl upload will prefill the description
1532 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001533 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1534 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001535
1536 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001537 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001538 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001539 name,
1540 description,
1541 absroot,
1542 files,
1543 issue,
1544 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001545 author,
1546 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001547
dsansomee2d6fd92016-09-08 00:10:47 -07001548 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001549 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001550 return self._codereview_impl.UpdateDescriptionRemote(
1551 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001552
1553 def RunHook(self, committing, may_prompt, verbose, change):
1554 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1555 try:
1556 return presubmit_support.DoPresubmitChecks(change, committing,
1557 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1558 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001559 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1560 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001561 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562 DieWithError(
1563 ('%s\nMaybe your depot_tools is out of date?\n'
1564 'If all fails, contact maruel@') % e)
1565
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001566 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1567 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001568 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1569 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001570 else:
1571 # Assume url.
1572 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1573 urlparse.urlparse(issue_arg))
1574 if not parsed_issue_arg or not parsed_issue_arg.valid:
1575 DieWithError('Failed to parse issue argument "%s". '
1576 'Must be an issue number or a valid URL.' % issue_arg)
1577 return self._codereview_impl.CMDPatchWithParsedIssue(
1578 parsed_issue_arg, reject, nocommit, directory)
1579
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 def CMDUpload(self, options, git_diff_args, orig_args):
1581 """Uploads a change to codereview."""
1582 if git_diff_args:
1583 # TODO(ukai): is it ok for gerrit case?
1584 base_branch = git_diff_args[0]
1585 else:
1586 if self.GetBranch() is None:
1587 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1588
1589 # Default to diffing against common ancestor of upstream branch
1590 base_branch = self.GetCommonAncestorWithUpstream()
1591 git_diff_args = [base_branch, 'HEAD']
1592
1593 # Make sure authenticated to codereview before running potentially expensive
1594 # hooks. It is a fast, best efforts check. Codereview still can reject the
1595 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001596 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001597
1598 # Apply watchlists on upload.
1599 change = self.GetChange(base_branch, None)
1600 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1601 files = [f.LocalPath() for f in change.AffectedFiles()]
1602 if not options.bypass_watchlists:
1603 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1604
1605 if not options.bypass_hooks:
1606 if options.reviewers or options.tbr_owners:
1607 # Set the reviewer list now so that presubmit checks can access it.
1608 change_description = ChangeDescription(change.FullDescriptionText())
1609 change_description.update_reviewers(options.reviewers,
1610 options.tbr_owners,
1611 change)
1612 change.SetDescriptionText(change_description.description)
1613 hook_results = self.RunHook(committing=False,
1614 may_prompt=not options.force,
1615 verbose=options.verbose,
1616 change=change)
1617 if not hook_results.should_continue():
1618 return 1
1619 if not options.reviewers and hook_results.reviewers:
1620 options.reviewers = hook_results.reviewers.split(',')
1621
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001622 # TODO(tandrii): Checking local patchset against remote patchset is only
1623 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1624 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625 latest_patchset = self.GetMostRecentPatchset()
1626 local_patchset = self.GetPatchset()
1627 if (latest_patchset and local_patchset and
1628 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001629 print('The last upload made from this repository was patchset #%d but '
1630 'the most recent patchset on the server is #%d.'
1631 % (local_patchset, latest_patchset))
1632 print('Uploading will still work, but if you\'ve uploaded to this '
1633 'issue from another machine or branch the patch you\'re '
1634 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001635 ask_for_data('About to upload; enter to confirm.')
1636
1637 print_stats(options.similarity, options.find_copies, git_diff_args)
1638 ret = self.CMDUploadChange(options, git_diff_args, change)
1639 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001640 if options.use_commit_queue:
1641 self.SetCQState(_CQState.COMMIT)
1642 elif options.cq_dry_run:
1643 self.SetCQState(_CQState.DRY_RUN)
1644
tandrii5d48c322016-08-18 16:19:37 -07001645 _git_set_branch_config_value('last-upload-hash',
1646 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001647 # Run post upload hooks, if specified.
1648 if settings.GetRunPostUploadHook():
1649 presubmit_support.DoPostUploadExecuter(
1650 change,
1651 self,
1652 settings.GetRoot(),
1653 options.verbose,
1654 sys.stdout)
1655
1656 # Upload all dependencies if specified.
1657 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001658 print()
1659 print('--dependencies has been specified.')
1660 print('All dependent local branches will be re-uploaded.')
1661 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001662 # Remove the dependencies flag from args so that we do not end up in a
1663 # loop.
1664 orig_args.remove('--dependencies')
1665 ret = upload_branch_deps(self, orig_args)
1666 return ret
1667
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001668 def SetCQState(self, new_state):
1669 """Update the CQ state for latest patchset.
1670
1671 Issue must have been already uploaded and known.
1672 """
1673 assert new_state in _CQState.ALL_STATES
1674 assert self.GetIssue()
1675 return self._codereview_impl.SetCQState(new_state)
1676
qyearsley1fdfcb62016-10-24 13:22:03 -07001677 def TriggerDryRun(self):
1678 """Triggers a dry run and prints a warning on failure."""
1679 # TODO(qyearsley): Either re-use this method in CMDset_commit
1680 # and CMDupload, or change CMDtry to trigger dry runs with
1681 # just SetCQState, and catch keyboard interrupt and other
1682 # errors in that method.
1683 try:
1684 self.SetCQState(_CQState.DRY_RUN)
1685 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1686 return 0
1687 except KeyboardInterrupt:
1688 raise
1689 except:
1690 print('WARNING: failed to trigger CQ Dry Run.\n'
1691 'Either:\n'
1692 ' * your project has no CQ\n'
1693 ' * you don\'t have permission to trigger Dry Run\n'
1694 ' * bug in this code (see stack trace below).\n'
1695 'Consider specifying which bots to trigger manually '
1696 'or asking your project owners for permissions '
1697 'or contacting Chrome Infrastructure team at '
1698 'https://www.chromium.org/infra\n\n')
1699 # Still raise exception so that stack trace is printed.
1700 raise
1701
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 # Forward methods to codereview specific implementation.
1703
1704 def CloseIssue(self):
1705 return self._codereview_impl.CloseIssue()
1706
1707 def GetStatus(self):
1708 return self._codereview_impl.GetStatus()
1709
1710 def GetCodereviewServer(self):
1711 return self._codereview_impl.GetCodereviewServer()
1712
tandriide281ae2016-10-12 06:02:30 -07001713 def GetIssueOwner(self):
1714 """Get owner from codereview, which may differ from this checkout."""
1715 return self._codereview_impl.GetIssueOwner()
1716
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 def GetApprovingReviewers(self):
1718 return self._codereview_impl.GetApprovingReviewers()
1719
1720 def GetMostRecentPatchset(self):
1721 return self._codereview_impl.GetMostRecentPatchset()
1722
tandriide281ae2016-10-12 06:02:30 -07001723 def CannotTriggerTryJobReason(self):
1724 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1725 return self._codereview_impl.CannotTriggerTryJobReason()
1726
tandrii8c5a3532016-11-04 07:52:02 -07001727 def GetTryjobProperties(self, patchset=None):
1728 """Returns dictionary of properties to launch tryjob."""
1729 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1730
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001731 def __getattr__(self, attr):
1732 # This is because lots of untested code accesses Rietveld-specific stuff
1733 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001734 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001735 # Note that child method defines __getattr__ as well, and forwards it here,
1736 # because _RietveldChangelistImpl is not cleaned up yet, and given
1737 # deprecation of Rietveld, it should probably be just removed.
1738 # Until that time, avoid infinite recursion by bypassing __getattr__
1739 # of implementation class.
1740 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741
1742
1743class _ChangelistCodereviewBase(object):
1744 """Abstract base class encapsulating codereview specifics of a changelist."""
1745 def __init__(self, changelist):
1746 self._changelist = changelist # instance of Changelist
1747
1748 def __getattr__(self, attr):
1749 # Forward methods to changelist.
1750 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1751 # _RietveldChangelistImpl to avoid this hack?
1752 return getattr(self._changelist, attr)
1753
1754 def GetStatus(self):
1755 """Apply a rough heuristic to give a simple summary of an issue's review
1756 or CQ status, assuming adherence to a common workflow.
1757
1758 Returns None if no issue for this branch, or specific string keywords.
1759 """
1760 raise NotImplementedError()
1761
1762 def GetCodereviewServer(self):
1763 """Returns server URL without end slash, like "https://codereview.com"."""
1764 raise NotImplementedError()
1765
1766 def FetchDescription(self):
1767 """Fetches and returns description from the codereview server."""
1768 raise NotImplementedError()
1769
tandrii5d48c322016-08-18 16:19:37 -07001770 @classmethod
1771 def IssueConfigKey(cls):
1772 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001773 raise NotImplementedError()
1774
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001775 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001776 def PatchsetConfigKey(cls):
1777 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001778 raise NotImplementedError()
1779
tandrii5d48c322016-08-18 16:19:37 -07001780 @classmethod
1781 def CodereviewServerConfigKey(cls):
1782 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 raise NotImplementedError()
1784
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001785 def _PostUnsetIssueProperties(self):
1786 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001787 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001788
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 def GetRieveldObjForPresubmit(self):
1790 # This is an unfortunate Rietveld-embeddedness in presubmit.
1791 # For non-Rietveld codereviews, this probably should return a dummy object.
1792 raise NotImplementedError()
1793
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001794 def GetGerritObjForPresubmit(self):
1795 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1796 return None
1797
dsansomee2d6fd92016-09-08 00:10:47 -07001798 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 """Update the description on codereview site."""
1800 raise NotImplementedError()
1801
1802 def CloseIssue(self):
1803 """Closes the issue."""
1804 raise NotImplementedError()
1805
1806 def GetApprovingReviewers(self):
1807 """Returns a list of reviewers approving the change.
1808
1809 Note: not necessarily committers.
1810 """
1811 raise NotImplementedError()
1812
1813 def GetMostRecentPatchset(self):
1814 """Returns the most recent patchset number from the codereview site."""
1815 raise NotImplementedError()
1816
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001817 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1818 directory):
1819 """Fetches and applies the issue.
1820
1821 Arguments:
1822 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1823 reject: if True, reject the failed patch instead of switching to 3-way
1824 merge. Rietveld only.
1825 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1826 only.
1827 directory: switch to directory before applying the patch. Rietveld only.
1828 """
1829 raise NotImplementedError()
1830
1831 @staticmethod
1832 def ParseIssueURL(parsed_url):
1833 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1834 failed."""
1835 raise NotImplementedError()
1836
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001837 def EnsureAuthenticated(self, force):
1838 """Best effort check that user is authenticated with codereview server.
1839
1840 Arguments:
1841 force: whether to skip confirmation questions.
1842 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001843 raise NotImplementedError()
1844
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001845 def CMDUploadChange(self, options, args, change):
1846 """Uploads a change to codereview."""
1847 raise NotImplementedError()
1848
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001849 def SetCQState(self, new_state):
1850 """Update the CQ state for latest patchset.
1851
1852 Issue must have been already uploaded and known.
1853 """
1854 raise NotImplementedError()
1855
tandriie113dfd2016-10-11 10:20:12 -07001856 def CannotTriggerTryJobReason(self):
1857 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1858 raise NotImplementedError()
1859
tandriide281ae2016-10-12 06:02:30 -07001860 def GetIssueOwner(self):
1861 raise NotImplementedError()
1862
tandrii8c5a3532016-11-04 07:52:02 -07001863 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001864 raise NotImplementedError()
1865
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866
1867class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1868 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1869 super(_RietveldChangelistImpl, self).__init__(changelist)
1870 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001871 if not rietveld_server:
1872 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001873
1874 self._rietveld_server = rietveld_server
1875 self._auth_config = auth_config
1876 self._props = None
1877 self._rpc_server = None
1878
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001879 def GetCodereviewServer(self):
1880 if not self._rietveld_server:
1881 # If we're on a branch then get the server potentially associated
1882 # with that branch.
1883 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001884 self._rietveld_server = gclient_utils.UpgradeToHttps(
1885 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001886 if not self._rietveld_server:
1887 self._rietveld_server = settings.GetDefaultServerUrl()
1888 return self._rietveld_server
1889
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001890 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001891 """Best effort check that user is authenticated with Rietveld server."""
1892 if self._auth_config.use_oauth2:
1893 authenticator = auth.get_authenticator_for_host(
1894 self.GetCodereviewServer(), self._auth_config)
1895 if not authenticator.has_cached_credentials():
1896 raise auth.LoginRequiredError(self.GetCodereviewServer())
1897
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 def FetchDescription(self):
1899 issue = self.GetIssue()
1900 assert issue
1901 try:
1902 return self.RpcServer().get_description(issue).strip()
1903 except urllib2.HTTPError as e:
1904 if e.code == 404:
1905 DieWithError(
1906 ('\nWhile fetching the description for issue %d, received a '
1907 '404 (not found)\n'
1908 'error. It is likely that you deleted this '
1909 'issue on the server. If this is the\n'
1910 'case, please run\n\n'
1911 ' git cl issue 0\n\n'
1912 'to clear the association with the deleted issue. Then run '
1913 'this command again.') % issue)
1914 else:
1915 DieWithError(
1916 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1917 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001918 print('Warning: Failed to retrieve CL description due to network '
1919 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 return ''
1921
1922 def GetMostRecentPatchset(self):
1923 return self.GetIssueProperties()['patchsets'][-1]
1924
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 def GetIssueProperties(self):
1926 if self._props is None:
1927 issue = self.GetIssue()
1928 if not issue:
1929 self._props = {}
1930 else:
1931 self._props = self.RpcServer().get_issue_properties(issue, True)
1932 return self._props
1933
tandriie113dfd2016-10-11 10:20:12 -07001934 def CannotTriggerTryJobReason(self):
1935 props = self.GetIssueProperties()
1936 if not props:
1937 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1938 if props.get('closed'):
1939 return 'CL %s is closed' % self.GetIssue()
1940 if props.get('private'):
1941 return 'CL %s is private' % self.GetIssue()
1942 return None
1943
tandrii8c5a3532016-11-04 07:52:02 -07001944 def GetTryjobProperties(self, patchset=None):
1945 """Returns dictionary of properties to launch tryjob."""
1946 project = (self.GetIssueProperties() or {}).get('project')
1947 return {
1948 'issue': self.GetIssue(),
1949 'patch_project': project,
1950 'patch_storage': 'rietveld',
1951 'patchset': patchset or self.GetPatchset(),
1952 'rietveld': self.GetCodereviewServer(),
1953 }
1954
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001955 def GetApprovingReviewers(self):
1956 return get_approving_reviewers(self.GetIssueProperties())
1957
tandriide281ae2016-10-12 06:02:30 -07001958 def GetIssueOwner(self):
1959 return (self.GetIssueProperties() or {}).get('owner_email')
1960
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001961 def AddComment(self, message):
1962 return self.RpcServer().add_comment(self.GetIssue(), message)
1963
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001964 def GetStatus(self):
1965 """Apply a rough heuristic to give a simple summary of an issue's review
1966 or CQ status, assuming adherence to a common workflow.
1967
1968 Returns None if no issue for this branch, or one of the following keywords:
1969 * 'error' - error from review tool (including deleted issues)
1970 * 'unsent' - not sent for review
1971 * 'waiting' - waiting for review
1972 * 'reply' - waiting for owner to reply to review
1973 * 'lgtm' - LGTM from at least one approved reviewer
1974 * 'commit' - in the commit queue
1975 * 'closed' - closed
1976 """
1977 if not self.GetIssue():
1978 return None
1979
1980 try:
1981 props = self.GetIssueProperties()
1982 except urllib2.HTTPError:
1983 return 'error'
1984
1985 if props.get('closed'):
1986 # Issue is closed.
1987 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001988 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001989 # Issue is in the commit queue.
1990 return 'commit'
1991
1992 try:
1993 reviewers = self.GetApprovingReviewers()
1994 except urllib2.HTTPError:
1995 return 'error'
1996
1997 if reviewers:
1998 # Was LGTM'ed.
1999 return 'lgtm'
2000
2001 messages = props.get('messages') or []
2002
tandrii9d2c7a32016-06-22 03:42:45 -07002003 # Skip CQ messages that don't require owner's action.
2004 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2005 if 'Dry run:' in messages[-1]['text']:
2006 messages.pop()
2007 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2008 # This message always follows prior messages from CQ,
2009 # so skip this too.
2010 messages.pop()
2011 else:
2012 # This is probably a CQ messages warranting user attention.
2013 break
2014
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002015 if not messages:
2016 # No message was sent.
2017 return 'unsent'
2018 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002019 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002020 return 'reply'
2021 return 'waiting'
2022
dsansomee2d6fd92016-09-08 00:10:47 -07002023 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002024 return self.RpcServer().update_description(
2025 self.GetIssue(), self.description)
2026
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002028 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002030 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002031 return self.SetFlags({flag: value})
2032
2033 def SetFlags(self, flags):
2034 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002035 """
phajdan.jr68598232016-08-10 03:28:28 -07002036 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002037 try:
tandrii4b233bd2016-07-06 03:50:29 -07002038 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002039 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002040 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002041 if e.code == 404:
2042 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2043 if e.code == 403:
2044 DieWithError(
2045 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002046 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002047 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002049 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050 """Returns an upload.RpcServer() to access this review's rietveld instance.
2051 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002052 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002053 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002054 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002055 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002056 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002058 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002059 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002060 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061
tandrii5d48c322016-08-18 16:19:37 -07002062 @classmethod
2063 def PatchsetConfigKey(cls):
2064 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065
tandrii5d48c322016-08-18 16:19:37 -07002066 @classmethod
2067 def CodereviewServerConfigKey(cls):
2068 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002069
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002070 def GetRieveldObjForPresubmit(self):
2071 return self.RpcServer()
2072
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002073 def SetCQState(self, new_state):
2074 props = self.GetIssueProperties()
2075 if props.get('private'):
2076 DieWithError('Cannot set-commit on private issue')
2077
2078 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002079 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002080 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002081 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002082 else:
tandrii4b233bd2016-07-06 03:50:29 -07002083 assert new_state == _CQState.DRY_RUN
2084 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002085
2086
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002087 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2088 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002089 # PatchIssue should never be called with a dirty tree. It is up to the
2090 # caller to check this, but just in case we assert here since the
2091 # consequences of the caller not checking this could be dire.
2092 assert(not git_common.is_dirty_git_tree('apply'))
2093 assert(parsed_issue_arg.valid)
2094 self._changelist.issue = parsed_issue_arg.issue
2095 if parsed_issue_arg.hostname:
2096 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2097
skobes6468b902016-10-24 08:45:10 -07002098 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2099 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2100 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002101 try:
skobes6468b902016-10-24 08:45:10 -07002102 scm_obj.apply_patch(patchset_object)
2103 except Exception as e:
2104 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002105 return 1
2106
2107 # If we had an issue, commit the current state and register the issue.
2108 if not nocommit:
2109 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2110 'patch from issue %(i)s at patchset '
2111 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2112 % {'i': self.GetIssue(), 'p': patchset})])
2113 self.SetIssue(self.GetIssue())
2114 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002115 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002117 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 return 0
2119
2120 @staticmethod
2121 def ParseIssueURL(parsed_url):
2122 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2123 return None
wychen3c1c1722016-08-04 11:46:36 -07002124 # Rietveld patch: https://domain/<number>/#ps<patchset>
2125 match = re.match(r'/(\d+)/$', parsed_url.path)
2126 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2127 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002128 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002129 issue=int(match.group(1)),
2130 patchset=int(match2.group(1)),
2131 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002132 # Typical url: https://domain/<issue_number>[/[other]]
2133 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2134 if match:
skobes6468b902016-10-24 08:45:10 -07002135 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002136 issue=int(match.group(1)),
2137 hostname=parsed_url.netloc)
2138 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2139 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2140 if match:
skobes6468b902016-10-24 08:45:10 -07002141 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002142 issue=int(match.group(1)),
2143 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002144 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 return None
2146
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002147 def CMDUploadChange(self, options, args, change):
2148 """Upload the patch to Rietveld."""
2149 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2150 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2152 if options.emulate_svn_auto_props:
2153 upload_args.append('--emulate_svn_auto_props')
2154
2155 change_desc = None
2156
2157 if options.email is not None:
2158 upload_args.extend(['--email', options.email])
2159
2160 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002161 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002162 upload_args.extend(['--title', options.title])
2163 if options.message:
2164 upload_args.extend(['--message', options.message])
2165 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002166 print('This branch is associated with issue %s. '
2167 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002168 else:
nodirca166002016-06-27 10:59:51 -07002169 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002170 upload_args.extend(['--title', options.title])
2171 message = (options.title or options.message or
2172 CreateDescriptionFromLog(args))
2173 change_desc = ChangeDescription(message)
2174 if options.reviewers or options.tbr_owners:
2175 change_desc.update_reviewers(options.reviewers,
2176 options.tbr_owners,
2177 change)
2178 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002179 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002180
2181 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002182 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 return 1
2184
2185 upload_args.extend(['--message', change_desc.description])
2186 if change_desc.get_reviewers():
2187 upload_args.append('--reviewers=%s' % ','.join(
2188 change_desc.get_reviewers()))
2189 if options.send_mail:
2190 if not change_desc.get_reviewers():
2191 DieWithError("Must specify reviewers to send email.")
2192 upload_args.append('--send_mail')
2193
2194 # We check this before applying rietveld.private assuming that in
2195 # rietveld.cc only addresses which we can send private CLs to are listed
2196 # if rietveld.private is set, and so we should ignore rietveld.cc only
2197 # when --private is specified explicitly on the command line.
2198 if options.private:
2199 logging.warn('rietveld.cc is ignored since private flag is specified. '
2200 'You need to review and add them manually if necessary.')
2201 cc = self.GetCCListWithoutDefault()
2202 else:
2203 cc = self.GetCCList()
2204 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002205 if change_desc.get_cced():
2206 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 if cc:
2208 upload_args.extend(['--cc', cc])
2209
2210 if options.private or settings.GetDefaultPrivateFlag() == "True":
2211 upload_args.append('--private')
2212
2213 upload_args.extend(['--git_similarity', str(options.similarity)])
2214 if not options.find_copies:
2215 upload_args.extend(['--git_no_find_copies'])
2216
2217 # Include the upstream repo's URL in the change -- this is useful for
2218 # projects that have their source spread across multiple repos.
2219 remote_url = self.GetGitBaseUrlFromConfig()
2220 if not remote_url:
2221 if settings.GetIsGitSvn():
2222 remote_url = self.GetGitSvnRemoteUrl()
2223 else:
2224 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2225 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2226 self.GetUpstreamBranch().split('/')[-1])
2227 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 remote, remote_branch = self.GetRemoteBranch()
2229 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2230 settings.GetPendingRefPrefix())
2231 if target_ref:
2232 upload_args.extend(['--target_ref', target_ref])
2233
2234 # Look for dependent patchsets. See crbug.com/480453 for more details.
2235 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2236 upstream_branch = ShortBranchName(upstream_branch)
2237 if remote is '.':
2238 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002239 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002241 print()
2242 print('Skipping dependency patchset upload because git config '
2243 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2244 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002245 else:
2246 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002247 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 auth_config=auth_config)
2249 branch_cl_issue_url = branch_cl.GetIssueURL()
2250 branch_cl_issue = branch_cl.GetIssue()
2251 branch_cl_patchset = branch_cl.GetPatchset()
2252 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2253 upload_args.extend(
2254 ['--depends_on_patchset', '%s:%s' % (
2255 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002256 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 '\n'
2258 'The current branch (%s) is tracking a local branch (%s) with '
2259 'an associated CL.\n'
2260 'Adding %s/#ps%s as a dependency patchset.\n'
2261 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2262 branch_cl_patchset))
2263
2264 project = settings.GetProject()
2265 if project:
2266 upload_args.extend(['--project', project])
2267
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 try:
2269 upload_args = ['upload'] + upload_args + args
2270 logging.info('upload.RealMain(%s)', upload_args)
2271 issue, patchset = upload.RealMain(upload_args)
2272 issue = int(issue)
2273 patchset = int(patchset)
2274 except KeyboardInterrupt:
2275 sys.exit(1)
2276 except:
2277 # If we got an exception after the user typed a description for their
2278 # change, back up the description before re-raising.
2279 if change_desc:
2280 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2281 print('\nGot exception while uploading -- saving description to %s\n' %
2282 backup_path)
2283 backup_file = open(backup_path, 'w')
2284 backup_file.write(change_desc.description)
2285 backup_file.close()
2286 raise
2287
2288 if not self.GetIssue():
2289 self.SetIssue(issue)
2290 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 return 0
2292
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002293
2294class _GerritChangelistImpl(_ChangelistCodereviewBase):
2295 def __init__(self, changelist, auth_config=None):
2296 # auth_config is Rietveld thing, kept here to preserve interface only.
2297 super(_GerritChangelistImpl, self).__init__(changelist)
2298 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002299 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002300 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002301 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002302
2303 def _GetGerritHost(self):
2304 # Lazy load of configs.
2305 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002306 if self._gerrit_host and '.' not in self._gerrit_host:
2307 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2308 # This happens for internal stuff http://crbug.com/614312.
2309 parsed = urlparse.urlparse(self.GetRemoteUrl())
2310 if parsed.scheme == 'sso':
2311 print('WARNING: using non https URLs for remote is likely broken\n'
2312 ' Your current remote is: %s' % self.GetRemoteUrl())
2313 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2314 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002315 return self._gerrit_host
2316
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002317 def _GetGitHost(self):
2318 """Returns git host to be used when uploading change to Gerrit."""
2319 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2320
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321 def GetCodereviewServer(self):
2322 if not self._gerrit_server:
2323 # If we're on a branch then get the server potentially associated
2324 # with that branch.
2325 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002326 self._gerrit_server = self._GitGetBranchConfigValue(
2327 self.CodereviewServerConfigKey())
2328 if self._gerrit_server:
2329 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002330 if not self._gerrit_server:
2331 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2332 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002333 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002334 parts[0] = parts[0] + '-review'
2335 self._gerrit_host = '.'.join(parts)
2336 self._gerrit_server = 'https://%s' % self._gerrit_host
2337 return self._gerrit_server
2338
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002339 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002340 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002341 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002342
tandrii5d48c322016-08-18 16:19:37 -07002343 @classmethod
2344 def PatchsetConfigKey(cls):
2345 return 'gerritpatchset'
2346
2347 @classmethod
2348 def CodereviewServerConfigKey(cls):
2349 return 'gerritserver'
2350
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002351 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002352 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002353 if settings.GetGerritSkipEnsureAuthenticated():
2354 # For projects with unusual authentication schemes.
2355 # See http://crbug.com/603378.
2356 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002357 # Lazy-loader to identify Gerrit and Git hosts.
2358 if gerrit_util.GceAuthenticator.is_gce():
2359 return
2360 self.GetCodereviewServer()
2361 git_host = self._GetGitHost()
2362 assert self._gerrit_server and self._gerrit_host
2363 cookie_auth = gerrit_util.CookiesAuthenticator()
2364
2365 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2366 git_auth = cookie_auth.get_auth_header(git_host)
2367 if gerrit_auth and git_auth:
2368 if gerrit_auth == git_auth:
2369 return
2370 print((
2371 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2372 ' Check your %s or %s file for credentials of hosts:\n'
2373 ' %s\n'
2374 ' %s\n'
2375 ' %s') %
2376 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2377 git_host, self._gerrit_host,
2378 cookie_auth.get_new_password_message(git_host)))
2379 if not force:
2380 ask_for_data('If you know what you are doing, press Enter to continue, '
2381 'Ctrl+C to abort.')
2382 return
2383 else:
2384 missing = (
2385 [] if gerrit_auth else [self._gerrit_host] +
2386 [] if git_auth else [git_host])
2387 DieWithError('Credentials for the following hosts are required:\n'
2388 ' %s\n'
2389 'These are read from %s (or legacy %s)\n'
2390 '%s' % (
2391 '\n '.join(missing),
2392 cookie_auth.get_gitcookies_path(),
2393 cookie_auth.get_netrc_path(),
2394 cookie_auth.get_new_password_message(git_host)))
2395
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002396 def _PostUnsetIssueProperties(self):
2397 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002398 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002399
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002400 def GetRieveldObjForPresubmit(self):
2401 class ThisIsNotRietveldIssue(object):
2402 def __nonzero__(self):
2403 # This is a hack to make presubmit_support think that rietveld is not
2404 # defined, yet still ensure that calls directly result in a decent
2405 # exception message below.
2406 return False
2407
2408 def __getattr__(self, attr):
2409 print(
2410 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2411 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2412 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2413 'or use Rietveld for codereview.\n'
2414 'See also http://crbug.com/579160.' % attr)
2415 raise NotImplementedError()
2416 return ThisIsNotRietveldIssue()
2417
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002418 def GetGerritObjForPresubmit(self):
2419 return presubmit_support.GerritAccessor(self._GetGerritHost())
2420
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002421 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002422 """Apply a rough heuristic to give a simple summary of an issue's review
2423 or CQ status, assuming adherence to a common workflow.
2424
2425 Returns None if no issue for this branch, or one of the following keywords:
2426 * 'error' - error from review tool (including deleted issues)
2427 * 'unsent' - no reviewers added
2428 * 'waiting' - waiting for review
2429 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002430 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2431 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002432 * 'commit' - in the commit queue
2433 * 'closed' - abandoned
2434 """
2435 if not self.GetIssue():
2436 return None
2437
2438 try:
2439 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002440 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002441 return 'error'
2442
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002443 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002444 return 'closed'
2445
2446 cq_label = data['labels'].get('Commit-Queue', {})
2447 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002448 votes = cq_label.get('all', [])
2449 highest_vote = 0
2450 for v in votes:
2451 highest_vote = max(highest_vote, v.get('value', 0))
2452 vote_value = str(highest_vote)
2453 if vote_value != '0':
2454 # Add a '+' if the value is not 0 to match the values in the label.
2455 # The cq_label does not have negatives.
2456 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002457 vote_text = cq_label.get('values', {}).get(vote_value, '')
2458 if vote_text.lower() == 'commit':
2459 return 'commit'
2460
2461 lgtm_label = data['labels'].get('Code-Review', {})
2462 if lgtm_label:
2463 if 'rejected' in lgtm_label:
2464 return 'not lgtm'
2465 if 'approved' in lgtm_label:
2466 return 'lgtm'
2467
2468 if not data.get('reviewers', {}).get('REVIEWER', []):
2469 return 'unsent'
2470
2471 messages = data.get('messages', [])
2472 if messages:
2473 owner = data['owner'].get('_account_id')
2474 last_message_author = messages[-1].get('author', {}).get('_account_id')
2475 if owner != last_message_author:
2476 # Some reply from non-owner.
2477 return 'reply'
2478
2479 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002480
2481 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002482 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002483 return data['revisions'][data['current_revision']]['_number']
2484
2485 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002486 data = self._GetChangeDetail(['CURRENT_REVISION'])
2487 current_rev = data['current_revision']
2488 url = data['revisions'][current_rev]['fetch']['http']['url']
2489 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002490
dsansomee2d6fd92016-09-08 00:10:47 -07002491 def UpdateDescriptionRemote(self, description, force=False):
2492 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2493 if not force:
2494 ask_for_data(
2495 'The description cannot be modified while the issue has a pending '
2496 'unpublished edit. Either publish the edit in the Gerrit web UI '
2497 'or delete it.\n\n'
2498 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2499
2500 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2501 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002502 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002503 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002504
2505 def CloseIssue(self):
2506 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2507
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002508 def GetApprovingReviewers(self):
2509 """Returns a list of reviewers approving the change.
2510
2511 Note: not necessarily committers.
2512 """
2513 raise NotImplementedError()
2514
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002515 def SubmitIssue(self, wait_for_merge=True):
2516 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2517 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002518
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 def _GetChangeDetail(self, options=None, issue=None):
2520 options = options or []
2521 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002522 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002523 try:
2524 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2525 options, ignore_404=False)
2526 except gerrit_util.GerritError as e:
2527 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002528 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002529 raise
tandriic2405f52016-10-10 08:13:15 -07002530 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002531
agable32978d92016-11-01 12:55:02 -07002532 def _GetChangeCommit(self, issue=None):
2533 issue = issue or self.GetIssue()
2534 assert issue, 'issue is required to query Gerrit'
2535 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2536 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002537 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002538 return data
2539
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002540 def CMDLand(self, force, bypass_hooks, verbose):
2541 if git_common.is_dirty_git_tree('land'):
2542 return 1
tandriid60367b2016-06-22 05:25:12 -07002543 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2544 if u'Commit-Queue' in detail.get('labels', {}):
2545 if not force:
2546 ask_for_data('\nIt seems this repository has a Commit Queue, '
2547 'which can test and land changes for you. '
2548 'Are you sure you wish to bypass it?\n'
2549 'Press Enter to continue, Ctrl+C to abort.')
2550
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002551 differs = True
tandriic4344b52016-08-29 06:04:54 -07002552 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002553 # Note: git diff outputs nothing if there is no diff.
2554 if not last_upload or RunGit(['diff', last_upload]).strip():
2555 print('WARNING: some changes from local branch haven\'t been uploaded')
2556 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002557 if detail['current_revision'] == last_upload:
2558 differs = False
2559 else:
2560 print('WARNING: local branch contents differ from latest uploaded '
2561 'patchset')
2562 if differs:
2563 if not force:
2564 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002565 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2566 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002567 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2568 elif not bypass_hooks:
2569 hook_results = self.RunHook(
2570 committing=True,
2571 may_prompt=not force,
2572 verbose=verbose,
2573 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2574 if not hook_results.should_continue():
2575 return 1
2576
2577 self.SubmitIssue(wait_for_merge=True)
2578 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002579 links = self._GetChangeCommit().get('web_links', [])
2580 for link in links:
2581 if link.get('name') == 'gerrit' and link.get('url'):
2582 print('Landed as %s' % link.get('url'))
2583 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002584 return 0
2585
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002586 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2587 directory):
2588 assert not reject
2589 assert not nocommit
2590 assert not directory
2591 assert parsed_issue_arg.valid
2592
2593 self._changelist.issue = parsed_issue_arg.issue
2594
2595 if parsed_issue_arg.hostname:
2596 self._gerrit_host = parsed_issue_arg.hostname
2597 self._gerrit_server = 'https://%s' % self._gerrit_host
2598
tandriic2405f52016-10-10 08:13:15 -07002599 try:
2600 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002601 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002602 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002603
2604 if not parsed_issue_arg.patchset:
2605 # Use current revision by default.
2606 revision_info = detail['revisions'][detail['current_revision']]
2607 patchset = int(revision_info['_number'])
2608 else:
2609 patchset = parsed_issue_arg.patchset
2610 for revision_info in detail['revisions'].itervalues():
2611 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2612 break
2613 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002614 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002615 (parsed_issue_arg.patchset, self.GetIssue()))
2616
2617 fetch_info = revision_info['fetch']['http']
2618 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2619 RunGit(['cherry-pick', 'FETCH_HEAD'])
2620 self.SetIssue(self.GetIssue())
2621 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002622 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002623 (self.GetIssue(), self.GetPatchset()))
2624 return 0
2625
2626 @staticmethod
2627 def ParseIssueURL(parsed_url):
2628 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2629 return None
2630 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2631 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2632 # Short urls like https://domain/<issue_number> can be used, but don't allow
2633 # specifying the patchset (you'd 404), but we allow that here.
2634 if parsed_url.path == '/':
2635 part = parsed_url.fragment
2636 else:
2637 part = parsed_url.path
2638 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2639 if match:
2640 return _ParsedIssueNumberArgument(
2641 issue=int(match.group(2)),
2642 patchset=int(match.group(4)) if match.group(4) else None,
2643 hostname=parsed_url.netloc)
2644 return None
2645
tandrii16e0b4e2016-06-07 10:34:28 -07002646 def _GerritCommitMsgHookCheck(self, offer_removal):
2647 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2648 if not os.path.exists(hook):
2649 return
2650 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2651 # custom developer made one.
2652 data = gclient_utils.FileRead(hook)
2653 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2654 return
2655 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002656 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002657 'and may interfere with it in subtle ways.\n'
2658 'We recommend you remove the commit-msg hook.')
2659 if offer_removal:
2660 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2661 if reply.lower().startswith('y'):
2662 gclient_utils.rm_file_or_tree(hook)
2663 print('Gerrit commit-msg hook removed.')
2664 else:
2665 print('OK, will keep Gerrit commit-msg hook in place.')
2666
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 def CMDUploadChange(self, options, args, change):
2668 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002669 if options.squash and options.no_squash:
2670 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002671
2672 if not options.squash and not options.no_squash:
2673 # Load default for user, repo, squash=true, in this order.
2674 options.squash = settings.GetSquashGerritUploads()
2675 elif options.no_squash:
2676 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002677
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 # We assume the remote called "origin" is the one we want.
2679 # It is probably not worthwhile to support different workflows.
2680 gerrit_remote = 'origin'
2681
2682 remote, remote_branch = self.GetRemoteBranch()
2683 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2684 pending_prefix='')
2685
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002687 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002688 if self.GetIssue():
2689 # Try to get the message from a previous upload.
2690 message = self.GetDescription()
2691 if not message:
2692 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002693 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002694 '%s' % (self.GetIssue(), self.GetIssueURL()))
2695 change_id = self._GetChangeDetail()['change_id']
2696 while True:
2697 footer_change_ids = git_footers.get_footer_change_id(message)
2698 if footer_change_ids == [change_id]:
2699 break
2700 if not footer_change_ids:
2701 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002702 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 continue
2704 # There is already a valid footer but with different or several ids.
2705 # Doing this automatically is non-trivial as we don't want to lose
2706 # existing other footers, yet we want to append just 1 desired
2707 # Change-Id. Thus, just create a new footer, but let user verify the
2708 # new description.
2709 message = '%s\n\nChange-Id: %s' % (message, change_id)
2710 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002711 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002712 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002713 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002714 'Please, check the proposed correction to the description, '
2715 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2716 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2717 change_id))
2718 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2719 if not options.force:
2720 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002721 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722 message = change_desc.description
2723 if not message:
2724 DieWithError("Description is empty. Aborting...")
2725 # Continue the while loop.
2726 # Sanity check of this code - we should end up with proper message
2727 # footer.
2728 assert [change_id] == git_footers.get_footer_change_id(message)
2729 change_desc = ChangeDescription(message)
2730 else:
2731 change_desc = ChangeDescription(
2732 options.message or CreateDescriptionFromLog(args))
2733 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002734 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735 if not change_desc.description:
2736 DieWithError("Description is empty. Aborting...")
2737 message = change_desc.description
2738 change_ids = git_footers.get_footer_change_id(message)
2739 if len(change_ids) > 1:
2740 DieWithError('too many Change-Id footers, at most 1 allowed.')
2741 if not change_ids:
2742 # Generate the Change-Id automatically.
2743 message = git_footers.add_footer_change_id(
2744 message, GenerateGerritChangeId(message))
2745 change_desc.set_description(message)
2746 change_ids = git_footers.get_footer_change_id(message)
2747 assert len(change_ids) == 1
2748 change_id = change_ids[0]
2749
2750 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2751 if remote is '.':
2752 # If our upstream branch is local, we base our squashed commit on its
2753 # squashed version.
2754 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2755 # Check the squashed hash of the parent.
2756 parent = RunGit(['config',
2757 'branch.%s.gerritsquashhash' % upstream_branch_name],
2758 error_ok=True).strip()
2759 # Verify that the upstream branch has been uploaded too, otherwise
2760 # Gerrit will create additional CLs when uploading.
2761 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2762 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002763 DieWithError(
2764 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002765 'Note: maybe you\'ve uploaded it with --no-squash. '
2766 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002767 ' git cl upload --squash\n' % upstream_branch_name)
2768 else:
2769 parent = self.GetCommonAncestorWithUpstream()
2770
2771 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2772 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2773 '-m', message]).strip()
2774 else:
2775 change_desc = ChangeDescription(
2776 options.message or CreateDescriptionFromLog(args))
2777 if not change_desc.description:
2778 DieWithError("Description is empty. Aborting...")
2779
2780 if not git_footers.get_footer_change_id(change_desc.description):
2781 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002782 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2783 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002784 ref_to_push = 'HEAD'
2785 parent = '%s/%s' % (gerrit_remote, branch)
2786 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2787
2788 assert change_desc
2789 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2790 ref_to_push)]).splitlines()
2791 if len(commits) > 1:
2792 print('WARNING: This will upload %d commits. Run the following command '
2793 'to see which commits will be uploaded: ' % len(commits))
2794 print('git log %s..%s' % (parent, ref_to_push))
2795 print('You can also use `git squash-branch` to squash these into a '
2796 'single commit.')
2797 ask_for_data('About to upload; enter to confirm.')
2798
2799 if options.reviewers or options.tbr_owners:
2800 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2801 change)
2802
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002803 # Extra options that can be specified at push time. Doc:
2804 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2805 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002806 if change_desc.get_reviewers(tbr_only=True):
2807 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2808 refspec_opts.append('l=Code-Review+1')
2809
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002810 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002811 if not re.match(r'^[\w ]+$', options.title):
2812 options.title = re.sub(r'[^\w ]', '', options.title)
2813 print('WARNING: Patchset title may only contain alphanumeric chars '
2814 'and spaces. Cleaned up title:\n%s' % options.title)
2815 if not options.force:
2816 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002817 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2818 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002819 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2820
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002821 if options.send_mail:
2822 if not change_desc.get_reviewers():
2823 DieWithError('Must specify reviewers to send email.')
2824 refspec_opts.append('notify=ALL')
2825 else:
2826 refspec_opts.append('notify=NONE')
2827
tandrii99a72f22016-08-17 14:33:24 -07002828 reviewers = change_desc.get_reviewers()
2829 if reviewers:
2830 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002831
agablec6787972016-09-09 16:13:34 -07002832 if options.private:
2833 refspec_opts.append('draft')
2834
rmistry9eadede2016-09-19 11:22:43 -07002835 if options.topic:
2836 # Documentation on Gerrit topics is here:
2837 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2838 refspec_opts.append('topic=%s' % options.topic)
2839
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002840 refspec_suffix = ''
2841 if refspec_opts:
2842 refspec_suffix = '%' + ','.join(refspec_opts)
2843 assert ' ' not in refspec_suffix, (
2844 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002845 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002846
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002847 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002848 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002849 print_stdout=True,
2850 # Flush after every line: useful for seeing progress when running as
2851 # recipe.
2852 filter_fn=lambda _: sys.stdout.flush())
2853
2854 if options.squash:
2855 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2856 change_numbers = [m.group(1)
2857 for m in map(regex.match, push_stdout.splitlines())
2858 if m]
2859 if len(change_numbers) != 1:
2860 DieWithError(
2861 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2862 'Change-Id: %s') % (len(change_numbers), change_id))
2863 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002864 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002865
2866 # Add cc's from the CC_LIST and --cc flag (if any).
2867 cc = self.GetCCList().split(',')
2868 if options.cc:
2869 cc.extend(options.cc)
2870 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002871 if change_desc.get_cced():
2872 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002873 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002874 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07002875 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002876 return 0
2877
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002878 def _AddChangeIdToCommitMessage(self, options, args):
2879 """Re-commits using the current message, assumes the commit hook is in
2880 place.
2881 """
2882 log_desc = options.message or CreateDescriptionFromLog(args)
2883 git_command = ['commit', '--amend', '-m', log_desc]
2884 RunGit(git_command)
2885 new_log_desc = CreateDescriptionFromLog(args)
2886 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002887 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002888 return new_log_desc
2889 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002890 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002891
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002892 def SetCQState(self, new_state):
2893 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002894 vote_map = {
2895 _CQState.NONE: 0,
2896 _CQState.DRY_RUN: 1,
2897 _CQState.COMMIT : 2,
2898 }
2899 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2900 labels={'Commit-Queue': vote_map[new_state]})
2901
tandriie113dfd2016-10-11 10:20:12 -07002902 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002903 try:
2904 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002905 except GerritChangeNotExists:
2906 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002907
2908 if data['status'] in ('ABANDONED', 'MERGED'):
2909 return 'CL %s is closed' % self.GetIssue()
2910
2911 def GetTryjobProperties(self, patchset=None):
2912 """Returns dictionary of properties to launch tryjob."""
2913 data = self._GetChangeDetail(['ALL_REVISIONS'])
2914 patchset = int(patchset or self.GetPatchset())
2915 assert patchset
2916 revision_data = None # Pylint wants it to be defined.
2917 for revision_data in data['revisions'].itervalues():
2918 if int(revision_data['_number']) == patchset:
2919 break
2920 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002921 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002922 (patchset, self.GetIssue()))
2923 return {
2924 'patch_issue': self.GetIssue(),
2925 'patch_set': patchset or self.GetPatchset(),
2926 'patch_project': data['project'],
2927 'patch_storage': 'gerrit',
2928 'patch_ref': revision_data['fetch']['http']['ref'],
2929 'patch_repository_url': revision_data['fetch']['http']['url'],
2930 'patch_gerrit_url': self.GetCodereviewServer(),
2931 }
tandriie113dfd2016-10-11 10:20:12 -07002932
tandriide281ae2016-10-12 06:02:30 -07002933 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002934 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002935
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002936
2937_CODEREVIEW_IMPLEMENTATIONS = {
2938 'rietveld': _RietveldChangelistImpl,
2939 'gerrit': _GerritChangelistImpl,
2940}
2941
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002942
iannuccie53c9352016-08-17 14:40:40 -07002943def _add_codereview_issue_select_options(parser, extra=""):
2944 _add_codereview_select_options(parser)
2945
2946 text = ('Operate on this issue number instead of the current branch\'s '
2947 'implicit issue.')
2948 if extra:
2949 text += ' '+extra
2950 parser.add_option('-i', '--issue', type=int, help=text)
2951
2952
2953def _process_codereview_issue_select_options(parser, options):
2954 _process_codereview_select_options(parser, options)
2955 if options.issue is not None and not options.forced_codereview:
2956 parser.error('--issue must be specified with either --rietveld or --gerrit')
2957
2958
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002959def _add_codereview_select_options(parser):
2960 """Appends --gerrit and --rietveld options to force specific codereview."""
2961 parser.codereview_group = optparse.OptionGroup(
2962 parser, 'EXPERIMENTAL! Codereview override options')
2963 parser.add_option_group(parser.codereview_group)
2964 parser.codereview_group.add_option(
2965 '--gerrit', action='store_true',
2966 help='Force the use of Gerrit for codereview')
2967 parser.codereview_group.add_option(
2968 '--rietveld', action='store_true',
2969 help='Force the use of Rietveld for codereview')
2970
2971
2972def _process_codereview_select_options(parser, options):
2973 if options.gerrit and options.rietveld:
2974 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2975 options.forced_codereview = None
2976 if options.gerrit:
2977 options.forced_codereview = 'gerrit'
2978 elif options.rietveld:
2979 options.forced_codereview = 'rietveld'
2980
2981
tandriif9aefb72016-07-01 09:06:51 -07002982def _get_bug_line_values(default_project, bugs):
2983 """Given default_project and comma separated list of bugs, yields bug line
2984 values.
2985
2986 Each bug can be either:
2987 * a number, which is combined with default_project
2988 * string, which is left as is.
2989
2990 This function may produce more than one line, because bugdroid expects one
2991 project per line.
2992
2993 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2994 ['v8:123', 'chromium:789']
2995 """
2996 default_bugs = []
2997 others = []
2998 for bug in bugs.split(','):
2999 bug = bug.strip()
3000 if bug:
3001 try:
3002 default_bugs.append(int(bug))
3003 except ValueError:
3004 others.append(bug)
3005
3006 if default_bugs:
3007 default_bugs = ','.join(map(str, default_bugs))
3008 if default_project:
3009 yield '%s:%s' % (default_project, default_bugs)
3010 else:
3011 yield default_bugs
3012 for other in sorted(others):
3013 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3014 yield other
3015
3016
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003017class ChangeDescription(object):
3018 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003019 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003020 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003021 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003022 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003023
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003025 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003026
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 @property # www.logilab.org/ticket/89786
3028 def description(self): # pylint: disable=E0202
3029 return '\n'.join(self._description_lines)
3030
3031 def set_description(self, desc):
3032 if isinstance(desc, basestring):
3033 lines = desc.splitlines()
3034 else:
3035 lines = [line.rstrip() for line in desc]
3036 while lines and not lines[0]:
3037 lines.pop(0)
3038 while lines and not lines[-1]:
3039 lines.pop(-1)
3040 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003041
piman@chromium.org336f9122014-09-04 02:16:55 +00003042 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003043 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003044 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003045 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003046 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003047 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003048
agable@chromium.org42c20792013-09-12 17:34:49 +00003049 # Get the set of R= and TBR= lines and remove them from the desciption.
3050 regexp = re.compile(self.R_LINE)
3051 matches = [regexp.match(line) for line in self._description_lines]
3052 new_desc = [l for i, l in enumerate(self._description_lines)
3053 if not matches[i]]
3054 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003055
agable@chromium.org42c20792013-09-12 17:34:49 +00003056 # Construct new unified R= and TBR= lines.
3057 r_names = []
3058 tbr_names = []
3059 for match in matches:
3060 if not match:
3061 continue
3062 people = cleanup_list([match.group(2).strip()])
3063 if match.group(1) == 'TBR':
3064 tbr_names.extend(people)
3065 else:
3066 r_names.extend(people)
3067 for name in r_names:
3068 if name not in reviewers:
3069 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003070 if add_owners_tbr:
3071 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003072 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003073 all_reviewers = set(tbr_names + reviewers)
3074 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3075 all_reviewers)
3076 tbr_names.extend(owners_db.reviewers_for(missing_files,
3077 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3079 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3080
3081 # Put the new lines in the description where the old first R= line was.
3082 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3083 if 0 <= line_loc < len(self._description_lines):
3084 if new_tbr_line:
3085 self._description_lines.insert(line_loc, new_tbr_line)
3086 if new_r_line:
3087 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003088 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003089 if new_r_line:
3090 self.append_footer(new_r_line)
3091 if new_tbr_line:
3092 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093
tandriif9aefb72016-07-01 09:06:51 -07003094 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003095 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 self.set_description([
3097 '# Enter a description of the change.',
3098 '# This will be displayed on the codereview site.',
3099 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003100 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003101 '--------------------',
3102 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003103
agable@chromium.org42c20792013-09-12 17:34:49 +00003104 regexp = re.compile(self.BUG_LINE)
3105 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003106 prefix = settings.GetBugPrefix()
3107 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3108 for value in values:
3109 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3110 self.append_footer('BUG=%s' % value)
3111
agable@chromium.org42c20792013-09-12 17:34:49 +00003112 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003113 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003114 if not content:
3115 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003116 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003117
3118 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003119 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3120 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003121 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003122 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003123
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003124 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003125 """Adds a footer line to the description.
3126
3127 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3128 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3129 that Gerrit footers are always at the end.
3130 """
3131 parsed_footer_line = git_footers.parse_footer(line)
3132 if parsed_footer_line:
3133 # Line is a gerrit footer in the form: Footer-Key: any value.
3134 # Thus, must be appended observing Gerrit footer rules.
3135 self.set_description(
3136 git_footers.add_footer(self.description,
3137 key=parsed_footer_line[0],
3138 value=parsed_footer_line[1]))
3139 return
3140
3141 if not self._description_lines:
3142 self._description_lines.append(line)
3143 return
3144
3145 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3146 if gerrit_footers:
3147 # git_footers.split_footers ensures that there is an empty line before
3148 # actual (gerrit) footers, if any. We have to keep it that way.
3149 assert top_lines and top_lines[-1] == ''
3150 top_lines, separator = top_lines[:-1], top_lines[-1:]
3151 else:
3152 separator = [] # No need for separator if there are no gerrit_footers.
3153
3154 prev_line = top_lines[-1] if top_lines else ''
3155 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3156 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3157 top_lines.append('')
3158 top_lines.append(line)
3159 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003160
tandrii99a72f22016-08-17 14:33:24 -07003161 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003162 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003163 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003164 reviewers = [match.group(2).strip()
3165 for match in matches
3166 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003167 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003168
bradnelsond975b302016-10-23 12:20:23 -07003169 def get_cced(self):
3170 """Retrieves the list of reviewers."""
3171 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3172 cced = [match.group(2).strip() for match in matches if match]
3173 return cleanup_list(cced)
3174
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003175 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3176 """Updates this commit description given the parent.
3177
3178 This is essentially what Gnumbd used to do.
3179 Consult https://goo.gl/WMmpDe for more details.
3180 """
3181 assert parent_msg # No, orphan branch creation isn't supported.
3182 assert parent_hash
3183 assert dest_ref
3184 parent_footer_map = git_footers.parse_footers(parent_msg)
3185 # This will also happily parse svn-position, which GnumbD is no longer
3186 # supporting. While we'd generate correct footers, the verifier plugin
3187 # installed in Gerrit will block such commit (ie git push below will fail).
3188 parent_position = git_footers.get_position(parent_footer_map)
3189
3190 # Cherry-picks may have last line obscuring their prior footers,
3191 # from git_footers perspective. This is also what Gnumbd did.
3192 cp_line = None
3193 if (self._description_lines and
3194 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3195 cp_line = self._description_lines.pop()
3196
3197 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3198
3199 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3200 # user interference with actual footers we'd insert below.
3201 for i, (k, v) in enumerate(parsed_footers):
3202 if k.startswith('Cr-'):
3203 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3204
3205 # Add Position and Lineage footers based on the parent.
3206 lineage = parent_footer_map.get('Cr-Branched-From', [])
3207 if parent_position[0] == dest_ref:
3208 # Same branch as parent.
3209 number = int(parent_position[1]) + 1
3210 else:
3211 number = 1 # New branch, and extra lineage.
3212 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3213 int(parent_position[1])))
3214
3215 parsed_footers.append(('Cr-Commit-Position',
3216 '%s@{#%d}' % (dest_ref, number)))
3217 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3218
3219 self._description_lines = top_lines
3220 if cp_line:
3221 self._description_lines.append(cp_line)
3222 if self._description_lines[-1] != '':
3223 self._description_lines.append('') # Ensure footer separator.
3224 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3225
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003226
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003227def get_approving_reviewers(props):
3228 """Retrieves the reviewers that approved a CL from the issue properties with
3229 messages.
3230
3231 Note that the list may contain reviewers that are not committer, thus are not
3232 considered by the CQ.
3233 """
3234 return sorted(
3235 set(
3236 message['sender']
3237 for message in props['messages']
3238 if message['approval'] and message['sender'] in props['reviewers']
3239 )
3240 )
3241
3242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243def FindCodereviewSettingsFile(filename='codereview.settings'):
3244 """Finds the given file starting in the cwd and going up.
3245
3246 Only looks up to the top of the repository unless an
3247 'inherit-review-settings-ok' file exists in the root of the repository.
3248 """
3249 inherit_ok_file = 'inherit-review-settings-ok'
3250 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003251 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3253 root = '/'
3254 while True:
3255 if filename in os.listdir(cwd):
3256 if os.path.isfile(os.path.join(cwd, filename)):
3257 return open(os.path.join(cwd, filename))
3258 if cwd == root:
3259 break
3260 cwd = os.path.dirname(cwd)
3261
3262
3263def LoadCodereviewSettingsFromFile(fileobj):
3264 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003265 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267 def SetProperty(name, setting, unset_error_ok=False):
3268 fullname = 'rietveld.' + name
3269 if setting in keyvals:
3270 RunGit(['config', fullname, keyvals[setting]])
3271 else:
3272 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3273
tandrii48df5812016-10-17 03:55:37 -07003274 if not keyvals.get('GERRIT_HOST', False):
3275 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276 # Only server setting is required. Other settings can be absent.
3277 # In that case, we ignore errors raised during option deletion attempt.
3278 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003279 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3281 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003282 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003283 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003284 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3285 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003286 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003287 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003288 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003289 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3290 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003291
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003292 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003293 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003294
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003295 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003296 RunGit(['config', 'gerrit.squash-uploads',
3297 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003298
tandrii@chromium.org28253532016-04-14 13:46:56 +00003299 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003300 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003301 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3304 #should be of the form
3305 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3306 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3307 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3308 keyvals['ORIGIN_URL_CONFIG']])
3309
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003310
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003311def urlretrieve(source, destination):
3312 """urllib is broken for SSL connections via a proxy therefore we
3313 can't use urllib.urlretrieve()."""
3314 with open(destination, 'w') as f:
3315 f.write(urllib2.urlopen(source).read())
3316
3317
ukai@chromium.org712d6102013-11-27 00:52:58 +00003318def hasSheBang(fname):
3319 """Checks fname is a #! script."""
3320 with open(fname) as f:
3321 return f.read(2).startswith('#!')
3322
3323
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003324# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3325def DownloadHooks(*args, **kwargs):
3326 pass
3327
3328
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003329def DownloadGerritHook(force):
3330 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003331
3332 Args:
3333 force: True to update hooks. False to install hooks if not present.
3334 """
3335 if not settings.GetIsGerrit():
3336 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003337 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003338 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3339 if not os.access(dst, os.X_OK):
3340 if os.path.exists(dst):
3341 if not force:
3342 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003343 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003344 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003345 if not hasSheBang(dst):
3346 DieWithError('Not a script: %s\n'
3347 'You need to download from\n%s\n'
3348 'into .git/hooks/commit-msg and '
3349 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003350 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3351 except Exception:
3352 if os.path.exists(dst):
3353 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003354 DieWithError('\nFailed to download hooks.\n'
3355 'You need to download from\n%s\n'
3356 'into .git/hooks/commit-msg and '
3357 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003358
3359
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003360
3361def GetRietveldCodereviewSettingsInteractively():
3362 """Prompt the user for settings."""
3363 server = settings.GetDefaultServerUrl(error_ok=True)
3364 prompt = 'Rietveld server (host[:port])'
3365 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3366 newserver = ask_for_data(prompt + ':')
3367 if not server and not newserver:
3368 newserver = DEFAULT_SERVER
3369 if newserver:
3370 newserver = gclient_utils.UpgradeToHttps(newserver)
3371 if newserver != server:
3372 RunGit(['config', 'rietveld.server', newserver])
3373
3374 def SetProperty(initial, caption, name, is_url):
3375 prompt = caption
3376 if initial:
3377 prompt += ' ("x" to clear) [%s]' % initial
3378 new_val = ask_for_data(prompt + ':')
3379 if new_val == 'x':
3380 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3381 elif new_val:
3382 if is_url:
3383 new_val = gclient_utils.UpgradeToHttps(new_val)
3384 if new_val != initial:
3385 RunGit(['config', 'rietveld.' + name, new_val])
3386
3387 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3388 SetProperty(settings.GetDefaultPrivateFlag(),
3389 'Private flag (rietveld only)', 'private', False)
3390 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3391 'tree-status-url', False)
3392 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3393 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3394 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3395 'run-post-upload-hook', False)
3396
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003397@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003399 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400
tandrii5d0a0422016-09-14 06:24:35 -07003401 print('WARNING: git cl config works for Rietveld only')
3402 # TODO(tandrii): remove this once we switch to Gerrit.
3403 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003404 parser.add_option('--activate-update', action='store_true',
3405 help='activate auto-updating [rietveld] section in '
3406 '.git/config')
3407 parser.add_option('--deactivate-update', action='store_true',
3408 help='deactivate auto-updating [rietveld] section in '
3409 '.git/config')
3410 options, args = parser.parse_args(args)
3411
3412 if options.deactivate_update:
3413 RunGit(['config', 'rietveld.autoupdate', 'false'])
3414 return
3415
3416 if options.activate_update:
3417 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3418 return
3419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003420 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003421 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003422 return 0
3423
3424 url = args[0]
3425 if not url.endswith('codereview.settings'):
3426 url = os.path.join(url, 'codereview.settings')
3427
3428 # Load code review settings and download hooks (if available).
3429 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3430 return 0
3431
3432
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003433def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003434 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003435 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3436 branch = ShortBranchName(branchref)
3437 _, args = parser.parse_args(args)
3438 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003439 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003440 return RunGit(['config', 'branch.%s.base-url' % branch],
3441 error_ok=False).strip()
3442 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003443 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003444 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3445 error_ok=False).strip()
3446
3447
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003448def color_for_status(status):
3449 """Maps a Changelist status to color, for CMDstatus and other tools."""
3450 return {
3451 'unsent': Fore.RED,
3452 'waiting': Fore.BLUE,
3453 'reply': Fore.YELLOW,
3454 'lgtm': Fore.GREEN,
3455 'commit': Fore.MAGENTA,
3456 'closed': Fore.CYAN,
3457 'error': Fore.WHITE,
3458 }.get(status, Fore.WHITE)
3459
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003460
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003461def get_cl_statuses(changes, fine_grained, max_processes=None):
3462 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003463
3464 If fine_grained is true, this will fetch CL statuses from the server.
3465 Otherwise, simply indicate if there's a matching url for the given branches.
3466
3467 If max_processes is specified, it is used as the maximum number of processes
3468 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3469 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003470
3471 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003472 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003473 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003474 upload.verbosity = 0
3475
3476 if fine_grained:
3477 # Process one branch synchronously to work through authentication, then
3478 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003479 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003480 def fetch(cl):
3481 try:
3482 return (cl, cl.GetStatus())
3483 except:
3484 # See http://crbug.com/629863.
3485 logging.exception('failed to fetch status for %s:', cl)
3486 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003487 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003488
tandriiea9514a2016-08-17 12:32:37 -07003489 changes_to_fetch = changes[1:]
3490 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003491 # Exit early if there was only one branch to fetch.
3492 return
3493
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003494 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003495 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003496 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003497 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003498
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003499 fetched_cls = set()
3500 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003501 while True:
3502 try:
3503 row = it.next(timeout=5)
3504 except multiprocessing.TimeoutError:
3505 break
3506
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003507 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003508 yield row
3509
3510 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003511 for cl in set(changes_to_fetch) - fetched_cls:
3512 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003513
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003514 else:
3515 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003516 for cl in changes:
3517 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003518
rmistry@google.com2dd99862015-06-22 12:22:18 +00003519
3520def upload_branch_deps(cl, args):
3521 """Uploads CLs of local branches that are dependents of the current branch.
3522
3523 If the local branch dependency tree looks like:
3524 test1 -> test2.1 -> test3.1
3525 -> test3.2
3526 -> test2.2 -> test3.3
3527
3528 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3529 run on the dependent branches in this order:
3530 test2.1, test3.1, test3.2, test2.2, test3.3
3531
3532 Note: This function does not rebase your local dependent branches. Use it when
3533 you make a change to the parent branch that will not conflict with its
3534 dependent branches, and you would like their dependencies updated in
3535 Rietveld.
3536 """
3537 if git_common.is_dirty_git_tree('upload-branch-deps'):
3538 return 1
3539
3540 root_branch = cl.GetBranch()
3541 if root_branch is None:
3542 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3543 'Get on a branch!')
3544 if not cl.GetIssue() or not cl.GetPatchset():
3545 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3546 'patchset dependencies without an uploaded CL.')
3547
3548 branches = RunGit(['for-each-ref',
3549 '--format=%(refname:short) %(upstream:short)',
3550 'refs/heads'])
3551 if not branches:
3552 print('No local branches found.')
3553 return 0
3554
3555 # Create a dictionary of all local branches to the branches that are dependent
3556 # on it.
3557 tracked_to_dependents = collections.defaultdict(list)
3558 for b in branches.splitlines():
3559 tokens = b.split()
3560 if len(tokens) == 2:
3561 branch_name, tracked = tokens
3562 tracked_to_dependents[tracked].append(branch_name)
3563
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print()
3565 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003566 dependents = []
3567 def traverse_dependents_preorder(branch, padding=''):
3568 dependents_to_process = tracked_to_dependents.get(branch, [])
3569 padding += ' '
3570 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003572 dependents.append(dependent)
3573 traverse_dependents_preorder(dependent, padding)
3574 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003575 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003576
3577 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003579 return 0
3580
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print('This command will checkout all dependent branches and run '
3582 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003583 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3584
andybons@chromium.org962f9462016-02-03 20:00:42 +00003585 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003586 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003587 args.extend(['-t', 'Updated patchset dependency'])
3588
rmistry@google.com2dd99862015-06-22 12:22:18 +00003589 # Record all dependents that failed to upload.
3590 failures = {}
3591 # Go through all dependents, checkout the branch and upload.
3592 try:
3593 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print()
3595 print('--------------------------------------')
3596 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003597 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003598 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003599 try:
3600 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003601 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602 failures[dependent_branch] = 1
3603 except: # pylint: disable=W0702
3604 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003605 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003606 finally:
3607 # Swap back to the original root branch.
3608 RunGit(['checkout', '-q', root_branch])
3609
vapiera7fbd5a2016-06-16 09:17:49 -07003610 print()
3611 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003612 for dependent_branch in dependents:
3613 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print(' %s : %s' % (dependent_branch, upload_status))
3615 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003616
3617 return 0
3618
3619
kmarshall3bff56b2016-06-06 18:31:47 -07003620def CMDarchive(parser, args):
3621 """Archives and deletes branches associated with closed changelists."""
3622 parser.add_option(
3623 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003624 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003625 parser.add_option(
3626 '-f', '--force', action='store_true',
3627 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003628 parser.add_option(
3629 '-d', '--dry-run', action='store_true',
3630 help='Skip the branch tagging and removal steps.')
3631 parser.add_option(
3632 '-t', '--notags', action='store_true',
3633 help='Do not tag archived branches. '
3634 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003635
3636 auth.add_auth_options(parser)
3637 options, args = parser.parse_args(args)
3638 if args:
3639 parser.error('Unsupported args: %s' % ' '.join(args))
3640 auth_config = auth.extract_auth_config_from_options(options)
3641
3642 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3643 if not branches:
3644 return 0
3645
vapiera7fbd5a2016-06-16 09:17:49 -07003646 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003647 changes = [Changelist(branchref=b, auth_config=auth_config)
3648 for b in branches.splitlines()]
3649 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3650 statuses = get_cl_statuses(changes,
3651 fine_grained=True,
3652 max_processes=options.maxjobs)
3653 proposal = [(cl.GetBranch(),
3654 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3655 for cl, status in statuses
3656 if status == 'closed']
3657 proposal.sort()
3658
3659 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003660 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003661 return 0
3662
3663 current_branch = GetCurrentBranch()
3664
vapiera7fbd5a2016-06-16 09:17:49 -07003665 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003666 if options.notags:
3667 for next_item in proposal:
3668 print(' ' + next_item[0])
3669 else:
3670 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3671 for next_item in proposal:
3672 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003673
kmarshall9249e012016-08-23 12:02:16 -07003674 # Quit now on precondition failure or if instructed by the user, either
3675 # via an interactive prompt or by command line flags.
3676 if options.dry_run:
3677 print('\nNo changes were made (dry run).\n')
3678 return 0
3679 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003680 print('You are currently on a branch \'%s\' which is associated with a '
3681 'closed codereview issue, so archive cannot proceed. Please '
3682 'checkout another branch and run this command again.' %
3683 current_branch)
3684 return 1
kmarshall9249e012016-08-23 12:02:16 -07003685 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003686 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3687 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003688 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003689 return 1
3690
3691 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003692 if not options.notags:
3693 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003694 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003695
vapiera7fbd5a2016-06-16 09:17:49 -07003696 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003697
3698 return 0
3699
3700
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003702 """Show status of changelists.
3703
3704 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003705 - Red not sent for review or broken
3706 - Blue waiting for review
3707 - Yellow waiting for you to reply to review
3708 - Green LGTM'ed
3709 - Magenta in the commit queue
3710 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003711
3712 Also see 'git cl comments'.
3713 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003715 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003716 parser.add_option('-f', '--fast', action='store_true',
3717 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003718 parser.add_option(
3719 '-j', '--maxjobs', action='store', type=int,
3720 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003721
3722 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003723 _add_codereview_issue_select_options(
3724 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003725 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003726 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003727 if args:
3728 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003729 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730
iannuccie53c9352016-08-17 14:40:40 -07003731 if options.issue is not None and not options.field:
3732 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003735 cl = Changelist(auth_config=auth_config, issue=options.issue,
3736 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003737 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003738 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003739 elif options.field == 'id':
3740 issueid = cl.GetIssue()
3741 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003742 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003743 elif options.field == 'patch':
3744 patchset = cl.GetPatchset()
3745 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003746 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003747 elif options.field == 'status':
3748 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003749 elif options.field == 'url':
3750 url = cl.GetIssueURL()
3751 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003753 return 0
3754
3755 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3756 if not branches:
3757 print('No local branch found.')
3758 return 0
3759
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003760 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003761 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003762 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003763 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003764 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003765 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003766 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003767
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003768 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003769 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3770 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3771 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003772 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003773 c, status = output.next()
3774 branch_statuses[c.GetBranch()] = status
3775 status = branch_statuses.pop(branch)
3776 url = cl.GetIssueURL()
3777 if url and (not status or status == 'error'):
3778 # The issue probably doesn't exist anymore.
3779 url += ' (broken)'
3780
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003781 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003782 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003783 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003784 color = ''
3785 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003786 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003787 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003788 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003789 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003790
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003791 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003792 print()
3793 print('Current branch:',)
3794 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003795 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003796 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003797 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003798 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003799 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003800 print('Issue description:')
3801 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003802 return 0
3803
3804
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003805def colorize_CMDstatus_doc():
3806 """To be called once in main() to add colors to git cl status help."""
3807 colors = [i for i in dir(Fore) if i[0].isupper()]
3808
3809 def colorize_line(line):
3810 for color in colors:
3811 if color in line.upper():
3812 # Extract whitespaces first and the leading '-'.
3813 indent = len(line) - len(line.lstrip(' ')) + 1
3814 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3815 return line
3816
3817 lines = CMDstatus.__doc__.splitlines()
3818 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3819
3820
phajdan.jre328cf92016-08-22 04:12:17 -07003821def write_json(path, contents):
3822 with open(path, 'w') as f:
3823 json.dump(contents, f)
3824
3825
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003826@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003827def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003828 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829
3830 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003831 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003832 parser.add_option('-r', '--reverse', action='store_true',
3833 help='Lookup the branch(es) for the specified issues. If '
3834 'no issues are specified, all branches with mapped '
3835 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003836 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003837 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003838 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003839 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840
dnj@chromium.org406c4402015-03-03 17:22:28 +00003841 if options.reverse:
3842 branches = RunGit(['for-each-ref', 'refs/heads',
3843 '--format=%(refname:short)']).splitlines()
3844
3845 # Reverse issue lookup.
3846 issue_branch_map = {}
3847 for branch in branches:
3848 cl = Changelist(branchref=branch)
3849 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3850 if not args:
3851 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003852 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003853 for issue in args:
3854 if not issue:
3855 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003856 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003857 print('Branch for issue number %s: %s' % (
3858 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003859 if options.json:
3860 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003861 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003862 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003863 if len(args) > 0:
3864 try:
3865 issue = int(args[0])
3866 except ValueError:
3867 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003868 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003869 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003870 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003871 if options.json:
3872 write_json(options.json, {
3873 'issue': cl.GetIssue(),
3874 'issue_url': cl.GetIssueURL(),
3875 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 return 0
3877
3878
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003879def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003880 """Shows or posts review comments for any changelist."""
3881 parser.add_option('-a', '--add-comment', dest='comment',
3882 help='comment to add to an issue')
3883 parser.add_option('-i', dest='issue',
3884 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003885 parser.add_option('-j', '--json-file',
3886 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003887 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003888 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003889 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003890
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003891 issue = None
3892 if options.issue:
3893 try:
3894 issue = int(options.issue)
3895 except ValueError:
3896 DieWithError('A review issue id is expected to be a number')
3897
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003898 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003899
3900 if options.comment:
3901 cl.AddComment(options.comment)
3902 return 0
3903
3904 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003905 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003906 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003907 summary.append({
3908 'date': message['date'],
3909 'lgtm': False,
3910 'message': message['text'],
3911 'not_lgtm': False,
3912 'sender': message['sender'],
3913 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003914 if message['disapproval']:
3915 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003916 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003917 elif message['approval']:
3918 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003919 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003920 elif message['sender'] == data['owner_email']:
3921 color = Fore.MAGENTA
3922 else:
3923 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003924 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003925 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003926 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003927 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003928 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003929 if options.json_file:
3930 with open(options.json_file, 'wb') as f:
3931 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003932 return 0
3933
3934
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003935@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003936def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003937 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003938 parser.add_option('-d', '--display', action='store_true',
3939 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003940 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003941 help='New description to set for this issue (- for stdin, '
3942 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003943 parser.add_option('-f', '--force', action='store_true',
3944 help='Delete any unpublished Gerrit edits for this issue '
3945 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003946
3947 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003948 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003949 options, args = parser.parse_args(args)
3950 _process_codereview_select_options(parser, options)
3951
3952 target_issue = None
3953 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003954 target_issue = ParseIssueNumberArgument(args[0])
3955 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003956 parser.print_help()
3957 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003958
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003959 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003960
martiniss6eda05f2016-06-30 10:18:35 -07003961 kwargs = {
3962 'auth_config': auth_config,
3963 'codereview': options.forced_codereview,
3964 }
3965 if target_issue:
3966 kwargs['issue'] = target_issue.issue
3967 if options.forced_codereview == 'rietveld':
3968 kwargs['rietveld_server'] = target_issue.hostname
3969
3970 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003971
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003972 if not cl.GetIssue():
3973 DieWithError('This branch has no associated changelist.')
3974 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003975
smut@google.com34fb6b12015-07-13 20:03:26 +00003976 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003977 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003978 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003979
3980 if options.new_description:
3981 text = options.new_description
3982 if text == '-':
3983 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003984 elif text == '+':
3985 base_branch = cl.GetCommonAncestorWithUpstream()
3986 change = cl.GetChange(base_branch, None, local_description=True)
3987 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003988
3989 description.set_description(text)
3990 else:
3991 description.prompt()
3992
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003993 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003994 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003995 return 0
3996
3997
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998def CreateDescriptionFromLog(args):
3999 """Pulls out the commit log to use as a base for the CL description."""
4000 log_args = []
4001 if len(args) == 1 and not args[0].endswith('.'):
4002 log_args = [args[0] + '..']
4003 elif len(args) == 1 and args[0].endswith('...'):
4004 log_args = [args[0][:-1]]
4005 elif len(args) == 2:
4006 log_args = [args[0] + '..' + args[1]]
4007 else:
4008 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004009 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010
4011
thestig@chromium.org44202a22014-03-11 19:22:18 +00004012def CMDlint(parser, args):
4013 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004014 parser.add_option('--filter', action='append', metavar='-x,+y',
4015 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004016 auth.add_auth_options(parser)
4017 options, args = parser.parse_args(args)
4018 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004019
4020 # Access to a protected member _XX of a client class
4021 # pylint: disable=W0212
4022 try:
4023 import cpplint
4024 import cpplint_chromium
4025 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004026 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004027 return 1
4028
4029 # Change the current working directory before calling lint so that it
4030 # shows the correct base.
4031 previous_cwd = os.getcwd()
4032 os.chdir(settings.GetRoot())
4033 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004034 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004035 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4036 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004037 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004038 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004039 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004040
4041 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004042 command = args + files
4043 if options.filter:
4044 command = ['--filter=' + ','.join(options.filter)] + command
4045 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004046
4047 white_regex = re.compile(settings.GetLintRegex())
4048 black_regex = re.compile(settings.GetLintIgnoreRegex())
4049 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4050 for filename in filenames:
4051 if white_regex.match(filename):
4052 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004054 else:
4055 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4056 extra_check_functions)
4057 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004058 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004059 finally:
4060 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004061 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004062 if cpplint._cpplint_state.error_count != 0:
4063 return 1
4064 return 0
4065
4066
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004068 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004069 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004071 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004072 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004073 auth.add_auth_options(parser)
4074 options, args = parser.parse_args(args)
4075 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076
sbc@chromium.org71437c02015-04-09 19:29:40 +00004077 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004078 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004079 return 1
4080
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004081 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082 if args:
4083 base_branch = args[0]
4084 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004085 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004086 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004088 cl.RunHook(
4089 committing=not options.upload,
4090 may_prompt=False,
4091 verbose=options.verbose,
4092 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004093 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094
4095
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004096def GenerateGerritChangeId(message):
4097 """Returns Ixxxxxx...xxx change id.
4098
4099 Works the same way as
4100 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4101 but can be called on demand on all platforms.
4102
4103 The basic idea is to generate git hash of a state of the tree, original commit
4104 message, author/committer info and timestamps.
4105 """
4106 lines = []
4107 tree_hash = RunGitSilent(['write-tree'])
4108 lines.append('tree %s' % tree_hash.strip())
4109 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4110 if code == 0:
4111 lines.append('parent %s' % parent.strip())
4112 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4113 lines.append('author %s' % author.strip())
4114 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4115 lines.append('committer %s' % committer.strip())
4116 lines.append('')
4117 # Note: Gerrit's commit-hook actually cleans message of some lines and
4118 # whitespace. This code is not doing this, but it clearly won't decrease
4119 # entropy.
4120 lines.append(message)
4121 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4122 stdin='\n'.join(lines))
4123 return 'I%s' % change_hash.strip()
4124
4125
wittman@chromium.org455dc922015-01-26 20:15:50 +00004126def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4127 """Computes the remote branch ref to use for the CL.
4128
4129 Args:
4130 remote (str): The git remote for the CL.
4131 remote_branch (str): The git remote branch for the CL.
4132 target_branch (str): The target branch specified by the user.
4133 pending_prefix (str): The pending prefix from the settings.
4134 """
4135 if not (remote and remote_branch):
4136 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004137
wittman@chromium.org455dc922015-01-26 20:15:50 +00004138 if target_branch:
4139 # Cannonicalize branch references to the equivalent local full symbolic
4140 # refs, which are then translated into the remote full symbolic refs
4141 # below.
4142 if '/' not in target_branch:
4143 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4144 else:
4145 prefix_replacements = (
4146 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4147 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4148 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4149 )
4150 match = None
4151 for regex, replacement in prefix_replacements:
4152 match = re.search(regex, target_branch)
4153 if match:
4154 remote_branch = target_branch.replace(match.group(0), replacement)
4155 break
4156 if not match:
4157 # This is a branch path but not one we recognize; use as-is.
4158 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004159 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4160 # Handle the refs that need to land in different refs.
4161 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004162
wittman@chromium.org455dc922015-01-26 20:15:50 +00004163 # Create the true path to the remote branch.
4164 # Does the following translation:
4165 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4166 # * refs/remotes/origin/master -> refs/heads/master
4167 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4168 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4169 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4170 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4171 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4172 'refs/heads/')
4173 elif remote_branch.startswith('refs/remotes/branch-heads'):
4174 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4175 # If a pending prefix exists then replace refs/ with it.
4176 if pending_prefix:
4177 remote_branch = remote_branch.replace('refs/', pending_prefix)
4178 return remote_branch
4179
4180
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004181def cleanup_list(l):
4182 """Fixes a list so that comma separated items are put as individual items.
4183
4184 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4185 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4186 """
4187 items = sum((i.split(',') for i in l), [])
4188 stripped_items = (i.strip() for i in items)
4189 return sorted(filter(None, stripped_items))
4190
4191
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004192@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004193def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004194 """Uploads the current changelist to codereview.
4195
4196 Can skip dependency patchset uploads for a branch by running:
4197 git config branch.branch_name.skip-deps-uploads True
4198 To unset run:
4199 git config --unset branch.branch_name.skip-deps-uploads
4200 Can also set the above globally by using the --global flag.
4201 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004202 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4203 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004204 parser.add_option('--bypass-watchlists', action='store_true',
4205 dest='bypass_watchlists',
4206 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004207 parser.add_option('-f', action='store_true', dest='force',
4208 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004209 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004210 parser.add_option('-b', '--bug',
4211 help='pre-populate the bug number(s) for this issue. '
4212 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004213 parser.add_option('--message-file', dest='message_file',
4214 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004215 parser.add_option('-t', dest='title',
4216 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004217 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004218 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004219 help='reviewer email addresses')
4220 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004221 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004222 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004223 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004224 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004225 parser.add_option('--emulate_svn_auto_props',
4226 '--emulate-svn-auto-props',
4227 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004228 dest="emulate_svn_auto_props",
4229 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004230 parser.add_option('-c', '--use-commit-queue', action='store_true',
4231 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004232 parser.add_option('--private', action='store_true',
4233 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004234 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004235 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004236 metavar='TARGET',
4237 help='Apply CL to remote ref TARGET. ' +
4238 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004239 parser.add_option('--squash', action='store_true',
4240 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004241 parser.add_option('--no-squash', action='store_true',
4242 help='Don\'t squash multiple commits into one ' +
4243 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004244 parser.add_option('--topic', default=None,
4245 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004246 parser.add_option('--email', default=None,
4247 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004248 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4249 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004250 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4251 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004252 help='Send the patchset to do a CQ dry run right after '
4253 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004254 parser.add_option('--dependencies', action='store_true',
4255 help='Uploads CLs of all the local branches that depend on '
4256 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004257
rmistry@google.com2dd99862015-06-22 12:22:18 +00004258 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004259 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004260 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004261 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004262 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004263 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004264 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004265
sbc@chromium.org71437c02015-04-09 19:29:40 +00004266 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004267 return 1
4268
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004269 options.reviewers = cleanup_list(options.reviewers)
4270 options.cc = cleanup_list(options.cc)
4271
tandriib80458a2016-06-23 12:20:07 -07004272 if options.message_file:
4273 if options.message:
4274 parser.error('only one of --message and --message-file allowed.')
4275 options.message = gclient_utils.FileRead(options.message_file)
4276 options.message_file = None
4277
tandrii4d0545a2016-07-06 03:56:49 -07004278 if options.cq_dry_run and options.use_commit_queue:
4279 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4280
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004281 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4282 settings.GetIsGerrit()
4283
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004284 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004285 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004286
4287
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004288def IsSubmoduleMergeCommit(ref):
4289 # When submodules are added to the repo, we expect there to be a single
4290 # non-git-svn merge commit at remote HEAD with a signature comment.
4291 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004292 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004293 return RunGit(cmd) != ''
4294
4295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004297 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004299 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4300 upstream and closes the issue automatically and atomically.
4301
4302 Otherwise (in case of Rietveld):
4303 Squashes branch into a single commit.
Andrii Shyshkalov06a25022016-11-24 16:47:00 +01004304 Updates commit message with metadata (e.g. pointer to review).
4305 Pushes the code upstream.
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004306 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004307 """
4308 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4309 help='bypass upload presubmit hook')
4310 parser.add_option('-m', dest='message',
4311 help="override review description")
4312 parser.add_option('-f', action='store_true', dest='force',
4313 help="force yes to questions (don't prompt)")
4314 parser.add_option('-c', dest='contributor',
4315 help="external contributor for patch (appended to " +
4316 "description and used as author for git). Should be " +
4317 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004318 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004319 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004321 auth_config = auth.extract_auth_config_from_options(options)
4322
4323 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004324
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004325 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4326 if cl.IsGerrit():
4327 if options.message:
4328 # This could be implemented, but it requires sending a new patch to
4329 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4330 # Besides, Gerrit has the ability to change the commit message on submit
4331 # automatically, thus there is no need to support this option (so far?).
4332 parser.error('-m MESSAGE option is not supported for Gerrit.')
4333 if options.contributor:
4334 parser.error(
4335 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4336 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4337 'the contributor\'s "name <email>". If you can\'t upload such a '
4338 'commit for review, contact your repository admin and request'
4339 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004340 if not cl.GetIssue():
Aaron Gablea45ee112016-11-22 15:14:38 -08004341 DieWithError('You must upload the change first to Gerrit.\n'
tandrii73449b02016-09-14 06:27:24 -07004342 ' If you would rather have `git cl land` upload '
4343 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004344 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4345 options.verbose)
4346
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004347 current = cl.GetBranch()
4348 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4349 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004350 print()
4351 print('Attempting to push branch %r into another local branch!' % current)
4352 print()
4353 print('Either reparent this branch on top of origin/master:')
4354 print(' git reparent-branch --root')
4355 print()
4356 print('OR run `git rebase-update` if you think the parent branch is ')
4357 print('already committed.')
4358 print()
4359 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004360 return 1
4361
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004362 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004363 # Default to merging against our best guess of the upstream branch.
4364 args = [cl.GetUpstreamBranch()]
4365
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004366 if options.contributor:
4367 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004369 return 1
4370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004371 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004372 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373
sbc@chromium.org71437c02015-04-09 19:29:40 +00004374 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004375 return 1
4376
4377 # This rev-list syntax means "show all commits not in my branch that
4378 # are in base_branch".
4379 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4380 base_branch]).splitlines()
4381 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print('Base branch "%s" has %d commits '
4383 'not in this branch.' % (base_branch, len(upstream_commits)))
4384 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385 return 1
4386
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004387 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004388 svn_head = None
4389 if cmd == 'dcommit' or base_has_submodules:
4390 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4391 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004393 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004394 # If the base_head is a submodule merge commit, the first parent of the
4395 # base_head should be a git-svn commit, which is what we're interested in.
4396 base_svn_head = base_branch
4397 if base_has_submodules:
4398 base_svn_head += '^1'
4399
4400 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004402 print('This branch has %d additional commits not upstreamed yet.'
4403 % len(extra_commits.splitlines()))
4404 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4405 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406 return 1
4407
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004408 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004409 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004410 author = None
4411 if options.contributor:
4412 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004413 hook_results = cl.RunHook(
4414 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004415 may_prompt=not options.force,
4416 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004417 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004418 if not hook_results.should_continue():
4419 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004420
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004421 # Check the tree status if the tree status URL is set.
4422 status = GetTreeStatus()
4423 if 'closed' == status:
4424 print('The tree is closed. Please wait for it to reopen. Use '
4425 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4426 return 1
4427 elif 'unknown' == status:
4428 print('Unable to determine tree status. Please verify manually and '
4429 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4430 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004432 change_desc = ChangeDescription(options.message)
4433 if not change_desc.description and cl.GetIssue():
4434 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004436 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004437 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004438 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004439 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004440 print('No description set.')
4441 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004442 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004444 # Keep a separate copy for the commit message, because the commit message
4445 # contains the link to the Rietveld issue, while the Rietveld message contains
4446 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004447 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004448 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004449
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004450 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004451 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004452 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004453 # after it. Add a period on a new line to circumvent this. Also add a space
4454 # before the period to make sure that Gitiles continues to correctly resolve
4455 # the URL.
4456 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004457 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004458 commit_desc.append_footer('Patch from %s.' % options.contributor)
4459
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004460 print('Description:')
4461 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004463 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004465 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004467 # We want to squash all this branch's commits into one commit with the proper
4468 # description. We do this by doing a "reset --soft" to the base branch (which
4469 # keeps the working copy the same), then dcommitting that. If origin/master
4470 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4471 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004473 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4474 # Delete the branches if they exist.
4475 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4476 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4477 result = RunGitWithCode(showref_cmd)
4478 if result[0] == 0:
4479 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004480
4481 # We might be in a directory that's present in this branch but not in the
4482 # trunk. Move up to the top of the tree so that git commands that expect a
4483 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004484 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004485 if rel_base_path:
4486 os.chdir(rel_base_path)
4487
4488 # Stuff our change into the merge branch.
4489 # We wrap in a try...finally block so if anything goes wrong,
4490 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004491 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004492 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004493 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004494 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004495 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004496 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004497 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004498 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004499 RunGit(
4500 [
4501 'commit', '--author', options.contributor,
4502 '-m', commit_desc.description,
4503 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004505 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004506 if base_has_submodules:
4507 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4508 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4509 RunGit(['checkout', CHERRY_PICK_BRANCH])
4510 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004511 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004512 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004513 mirror = settings.GetGitMirror(remote)
4514 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004515 pending_prefix = settings.GetPendingRefPrefix()
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004516
4517 if ShouldGenerateGitNumberFooters():
4518 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4519 # is no pending ref to push to?
4520 logging.debug('Adding git number footers')
4521 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4522 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4523 branch)
Andrii Shyshkalova6695812016-12-06 17:47:09 +01004524 # Ensure timestamps are monotonically increasing.
4525 timestamp = max(1 + _get_committer_timestamp(merge_base),
4526 _get_committer_timestamp('HEAD'))
4527 _git_amend_head(commit_desc.description, timestamp)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004528 change_desc = ChangeDescription(commit_desc.description)
4529 # If gnumbd is sitll ON and we ultimately push to branch with
4530 # pending_prefix, gnumbd will modify footers we've just inserted with
4531 # 'Original-', which is annoying but still technically correct.
4532
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004533 if not pending_prefix or branch.startswith(pending_prefix):
4534 # If not using refs/pending/heads/* at all, or target ref is already set
4535 # to pending, then push to the target ref directly.
Andrii Shyshkalov813ec3c2016-11-24 17:06:01 +01004536 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4537 # in practise. I really tried to create a new branch tracking
4538 # refs/pending/heads/master directly and git cl land failed long before
4539 # reaching this. Disagree? Comment on http://crbug.com/642493.
4540 if pending_prefix:
4541 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4542 'Grab your .git/config, add instructions how to reproduce '
4543 'this, and post it to http://crbug.com/642493.\n'
4544 'The first reporter gets a free "Black Swan" book from '
4545 'tandrii@\n\n')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004546 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004547 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004548 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004549 else:
4550 # Cherry-pick the change on top of pending ref and then push it.
4551 assert branch.startswith('refs/'), branch
4552 assert pending_prefix[-1] == '/', pending_prefix
4553 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004554 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004555 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004556 if retcode == 0:
4557 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004558 else:
4559 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004560 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004561 'svn', 'dcommit',
4562 '-C%s' % options.similarity,
4563 '--no-rebase', '--rmdir',
4564 ]
4565 if settings.GetForceHttpsCommitUrl():
4566 # Allow forcing https commit URLs for some projects that don't allow
4567 # committing to http URLs (like Google Code).
4568 remote_url = cl.GetGitSvnRemoteUrl()
4569 if urlparse.urlparse(remote_url).scheme == 'http':
4570 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004571 cmd_args.append('--commit-url=%s' % remote_url)
4572 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004573 if 'Committed r' in output:
4574 revision = re.match(
4575 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4576 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004577 finally:
4578 # And then swap back to the original branch and clean up.
4579 RunGit(['checkout', '-q', cl.GetBranch()])
4580 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004581 if base_has_submodules:
4582 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004583
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004584 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004585 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004586 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004587
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004588 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004589 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004590 try:
4591 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4592 # We set pushed_to_pending to False, since it made it all the way to the
4593 # real ref.
4594 pushed_to_pending = False
4595 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004596 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004598 if cl.GetIssue():
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004599 # TODO(tandrii): figure out story of to pending + git numberer.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004600 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004601 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004602 if not to_pending:
4603 if viewvc_url and revision:
4604 change_desc.append_footer(
4605 'Committed: %s%s' % (viewvc_url, revision))
4606 elif revision:
4607 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004608 print('Closing issue '
4609 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004610 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004612 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004613 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004614 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004615 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004616 if options.bypass_hooks:
4617 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4618 else:
4619 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004620 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004621
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004622 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004623 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004624 print('The commit is in the pending queue (%s).' % pending_ref)
4625 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4626 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004627
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004628 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4629 if os.path.isfile(hook):
4630 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004631
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004632 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633
4634
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004635def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004636 print()
4637 print('Waiting for commit to be landed on %s...' % real_ref)
4638 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004639 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4640 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004641 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004642
4643 loop = 0
4644 while True:
4645 sys.stdout.write('fetching (%d)... \r' % loop)
4646 sys.stdout.flush()
4647 loop += 1
4648
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004649 if mirror:
4650 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004651 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4652 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4653 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4654 for commit in commits.splitlines():
4655 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004656 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004657 return commit
4658
4659 current_rev = to_rev
4660
4661
tandriibf429402016-09-14 07:09:12 -07004662def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004663 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4664
4665 Returns:
4666 (retcode of last operation, output log of last operation).
4667 """
4668 assert pending_ref.startswith('refs/'), pending_ref
4669 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4670 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4671 code = 0
4672 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004673 max_attempts = 3
4674 attempts_left = max_attempts
4675 while attempts_left:
4676 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004677 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004678 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004679
4680 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004681 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004682 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004683 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004684 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004685 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004686 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004687 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004688 continue
4689
4690 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004691 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004692 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004693 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004694 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004695 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4696 'the following files have merge conflicts:' % pending_ref)
4697 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4698 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004699 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004700 return code, out
4701
4702 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004703 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004704 code, out = RunGitWithCode(
4705 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4706 if code == 0:
4707 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004708 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004709 return code, out
4710
vapiera7fbd5a2016-06-16 09:17:49 -07004711 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004712 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004713 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004714 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004715 print('Fatal push error. Make sure your .netrc credentials and git '
4716 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004717 return code, out
4718
vapiera7fbd5a2016-06-16 09:17:49 -07004719 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004720 return code, out
4721
4722
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004723def IsFatalPushFailure(push_stdout):
4724 """True if retrying push won't help."""
4725 return '(prohibited by Gerrit)' in push_stdout
4726
4727
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004728@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004730 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004731 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004732 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004733 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004734 message = """This repository appears to be a git-svn mirror, but we
4735don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004736 else:
4737 message = """This doesn't appear to be an SVN repository.
4738If your project has a true, writeable git repository, you probably want to run
4739'git cl land' instead.
4740If your project has a git mirror of an upstream SVN master, you probably need
4741to run 'git svn init'.
4742
4743Using the wrong command might cause your commit to appear to succeed, and the
4744review to be closed, without actually landing upstream. If you choose to
4745proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004746 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004747 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004748 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4749 'Please let us know of this project you are committing to:'
4750 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004751 return SendUpstream(parser, args, 'dcommit')
4752
4753
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004754@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004755def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004756 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004757 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004758 print('This appears to be an SVN repository.')
4759 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004760 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004761 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004762 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004763
4764
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004765@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004766def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004767 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004768 parser.add_option('-b', dest='newbranch',
4769 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004770 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004771 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004772 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4773 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004774 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004775 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004776 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004777 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004778 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004779 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004780
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004781
4782 group = optparse.OptionGroup(
4783 parser,
4784 'Options for continuing work on the current issue uploaded from a '
4785 'different clone (e.g. different machine). Must be used independently '
4786 'from the other options. No issue number should be specified, and the '
4787 'branch must have an issue number associated with it')
4788 group.add_option('--reapply', action='store_true', dest='reapply',
4789 help='Reset the branch and reapply the issue.\n'
4790 'CAUTION: This will undo any local changes in this '
4791 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004792
4793 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004794 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004795 parser.add_option_group(group)
4796
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004797 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004798 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004799 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004800 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004801 auth_config = auth.extract_auth_config_from_options(options)
4802
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004803
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004804 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004805 if options.newbranch:
4806 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004807 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004808 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004809
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004810 cl = Changelist(auth_config=auth_config,
4811 codereview=options.forced_codereview)
4812 if not cl.GetIssue():
4813 parser.error('current branch must have an associated issue')
4814
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004815 upstream = cl.GetUpstreamBranch()
4816 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004817 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004818
4819 RunGit(['reset', '--hard', upstream])
4820 if options.pull:
4821 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004822
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004823 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4824 options.directory)
4825
4826 if len(args) != 1 or not args[0]:
4827 parser.error('Must specify issue number or url')
4828
4829 # We don't want uncommitted changes mixed up with the patch.
4830 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004831 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004833 if options.newbranch:
4834 if options.force:
4835 RunGit(['branch', '-D', options.newbranch],
4836 stderr=subprocess2.PIPE, error_ok=True)
4837 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004838 elif not GetCurrentBranch():
4839 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004840
4841 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4842
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004843 if cl.IsGerrit():
4844 if options.reject:
4845 parser.error('--reject is not supported with Gerrit codereview.')
4846 if options.nocommit:
4847 parser.error('--nocommit is not supported with Gerrit codereview.')
4848 if options.directory:
4849 parser.error('--directory is not supported with Gerrit codereview.')
4850
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004851 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004852 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004853
4854
4855def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004856 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004857 # Provide a wrapper for git svn rebase to help avoid accidental
4858 # git svn dcommit.
4859 # It's the only command that doesn't use parser at all since we just defer
4860 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004861
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004862 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004863
4864
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004865def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004866 """Fetches the tree status and returns either 'open', 'closed',
4867 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004868 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004869 if url:
4870 status = urllib2.urlopen(url).read().lower()
4871 if status.find('closed') != -1 or status == '0':
4872 return 'closed'
4873 elif status.find('open') != -1 or status == '1':
4874 return 'open'
4875 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004876 return 'unset'
4877
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004879def GetTreeStatusReason():
4880 """Fetches the tree status from a json url and returns the message
4881 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004882 url = settings.GetTreeStatusUrl()
4883 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004884 connection = urllib2.urlopen(json_url)
4885 status = json.loads(connection.read())
4886 connection.close()
4887 return status['message']
4888
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004890def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004891 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004892 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004893 status = GetTreeStatus()
4894 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004895 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004896 return 2
4897
vapiera7fbd5a2016-06-16 09:17:49 -07004898 print('The tree is %s' % status)
4899 print()
4900 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004901 if status != 'open':
4902 return 1
4903 return 0
4904
4905
maruel@chromium.org15192402012-09-06 12:38:29 +00004906def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004907 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004908 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004909 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004910 '-b', '--bot', action='append',
4911 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4912 'times to specify multiple builders. ex: '
4913 '"-b win_rel -b win_layout". See '
4914 'the try server waterfall for the builders name and the tests '
4915 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004916 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004917 '-B', '--bucket', default='',
4918 help=('Buildbucket bucket to send the try requests.'))
4919 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004920 '-m', '--master', default='',
4921 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004922 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004923 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004924 help='Revision to use for the try job; default: the revision will '
4925 'be determined by the try recipe that builder runs, which usually '
4926 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004927 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004928 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004929 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004930 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004931 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004932 '--project',
4933 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004934 'in recipe to determine to which repository or directory to '
4935 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004936 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004937 '-p', '--property', dest='properties', action='append', default=[],
4938 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004939 'key2=value2 etc. The value will be treated as '
4940 'json if decodable, or as string otherwise. '
4941 'NOTE: using this may make your try job not usable for CQ, '
4942 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004943 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004944 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4945 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004946 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004947 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004948 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004949 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004950
machenbach@chromium.org45453142015-09-15 08:45:22 +00004951 # Make sure that all properties are prop=value pairs.
4952 bad_params = [x for x in options.properties if '=' not in x]
4953 if bad_params:
4954 parser.error('Got properties with missing "=": %s' % bad_params)
4955
maruel@chromium.org15192402012-09-06 12:38:29 +00004956 if args:
4957 parser.error('Unknown arguments: %s' % args)
4958
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004959 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004960 if not cl.GetIssue():
4961 parser.error('Need to upload first')
4962
tandriie113dfd2016-10-11 10:20:12 -07004963 error_message = cl.CannotTriggerTryJobReason()
4964 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004965 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004966
borenet6c0efe62016-10-19 08:13:29 -07004967 if options.bucket and options.master:
4968 parser.error('Only one of --bucket and --master may be used.')
4969
qyearsley1fdfcb62016-10-24 13:22:03 -07004970 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004971
qyearsleydd49f942016-10-28 11:57:22 -07004972 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4973 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004974 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004975 if options.verbose:
4976 print('git cl try with no bots now defaults to CQ Dry Run.')
4977 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004978
borenet6c0efe62016-10-19 08:13:29 -07004979 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004980 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004981 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004982 'of bot requires an initial job from a parent (usually a builder). '
4983 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004984 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004985 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004986
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004987 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004988 # TODO(tandrii): Checking local patchset against remote patchset is only
4989 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4990 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004991 print('Warning: Codereview server has newer patchsets (%s) than most '
4992 'recent upload from local checkout (%s). Did a previous upload '
4993 'fail?\n'
4994 'By default, git cl try uses the latest patchset from '
4995 'codereview, continuing to use patchset %s.\n' %
4996 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004997
tandrii568043b2016-10-11 07:49:18 -07004998 try:
borenet6c0efe62016-10-19 08:13:29 -07004999 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5000 patchset)
tandrii568043b2016-10-11 07:49:18 -07005001 except BuildbucketResponseException as ex:
5002 print('ERROR: %s' % ex)
5003 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005004 return 0
5005
5006
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005007def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005008 """Prints info about try jobs associated with current CL."""
5009 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005010 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005011 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005012 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005013 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005014 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005015 '--color', action='store_true', default=setup_color.IS_TTY,
5016 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005017 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005018 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5019 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005020 group.add_option(
5021 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005022 parser.add_option_group(group)
5023 auth.add_auth_options(parser)
5024 options, args = parser.parse_args(args)
5025 if args:
5026 parser.error('Unrecognized args: %s' % ' '.join(args))
5027
5028 auth_config = auth.extract_auth_config_from_options(options)
5029 cl = Changelist(auth_config=auth_config)
5030 if not cl.GetIssue():
5031 parser.error('Need to upload first')
5032
tandrii221ab252016-10-06 08:12:04 -07005033 patchset = options.patchset
5034 if not patchset:
5035 patchset = cl.GetMostRecentPatchset()
5036 if not patchset:
5037 parser.error('Codereview doesn\'t know about issue %s. '
5038 'No access to issue or wrong issue number?\n'
5039 'Either upload first, or pass --patchset explicitely' %
5040 cl.GetIssue())
5041
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005042 # TODO(tandrii): Checking local patchset against remote patchset is only
5043 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5044 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005045 print('Warning: Codereview server has newer patchsets (%s) than most '
5046 'recent upload from local checkout (%s). Did a previous upload '
5047 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005048 'By default, git cl try-results uses the latest patchset from '
5049 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005050 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005051 try:
tandrii221ab252016-10-06 08:12:04 -07005052 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005053 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005054 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005055 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005056 if options.json:
5057 write_try_results_json(options.json, jobs)
5058 else:
5059 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005060 return 0
5061
5062
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005063@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005064def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005065 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005066 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005067 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005068 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005070 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005071 if args:
5072 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005073 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005074 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005075 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005076 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005077
5078 # Clear configured merge-base, if there is one.
5079 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005080 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005081 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005082 return 0
5083
5084
thestig@chromium.org00858c82013-12-02 23:08:03 +00005085def CMDweb(parser, args):
5086 """Opens the current CL in the web browser."""
5087 _, args = parser.parse_args(args)
5088 if args:
5089 parser.error('Unrecognized args: %s' % ' '.join(args))
5090
5091 issue_url = Changelist().GetIssueURL()
5092 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005093 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005094 return 1
5095
5096 webbrowser.open(issue_url)
5097 return 0
5098
5099
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005100def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005101 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005102 parser.add_option('-d', '--dry-run', action='store_true',
5103 help='trigger in dry run mode')
5104 parser.add_option('-c', '--clear', action='store_true',
5105 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005106 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005107 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005108 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005109 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005110 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005111 if args:
5112 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005113 if options.dry_run and options.clear:
5114 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5115
iannuccie53c9352016-08-17 14:40:40 -07005116 cl = Changelist(auth_config=auth_config, issue=options.issue,
5117 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005118 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005119 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005120 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005121 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005122 state = _CQState.DRY_RUN
5123 else:
5124 state = _CQState.COMMIT
5125 if not cl.GetIssue():
5126 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005127 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005128 return 0
5129
5130
groby@chromium.org411034a2013-02-26 15:12:01 +00005131def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005132 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005133 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005134 auth.add_auth_options(parser)
5135 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005136 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005137 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005138 if args:
5139 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005140 cl = Changelist(auth_config=auth_config, issue=options.issue,
5141 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005142 # Ensure there actually is an issue to close.
5143 cl.GetDescription()
5144 cl.CloseIssue()
5145 return 0
5146
5147
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005148def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005149 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005150 parser.add_option(
5151 '--stat',
5152 action='store_true',
5153 dest='stat',
5154 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005155 auth.add_auth_options(parser)
5156 options, args = parser.parse_args(args)
5157 auth_config = auth.extract_auth_config_from_options(options)
5158 if args:
5159 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005160
5161 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005162 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005163 # Staged changes would be committed along with the patch from last
5164 # upload, hence counted toward the "last upload" side in the final
5165 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005166 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005167 return 1
5168
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005169 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005170 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005171 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005172 if not issue:
5173 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005174 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005175 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005176
5177 # Create a new branch based on the merge-base
5178 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005179 # Clear cached branch in cl object, to avoid overwriting original CL branch
5180 # properties.
5181 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005182 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005183 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005184 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005185 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005186 return rtn
5187
wychen@chromium.org06928532015-02-03 02:11:29 +00005188 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005189 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005190 cmd = ['git', 'diff']
5191 if options.stat:
5192 cmd.append('--stat')
5193 cmd.extend([TMP_BRANCH, branch, '--'])
5194 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005195 finally:
5196 RunGit(['checkout', '-q', branch])
5197 RunGit(['branch', '-D', TMP_BRANCH])
5198
5199 return 0
5200
5201
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005202def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005203 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005204 parser.add_option(
5205 '--no-color',
5206 action='store_true',
5207 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005208 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005209 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005210 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005211
5212 author = RunGit(['config', 'user.email']).strip() or None
5213
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005214 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005215
5216 if args:
5217 if len(args) > 1:
5218 parser.error('Unknown args')
5219 base_branch = args[0]
5220 else:
5221 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005222 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005223
5224 change = cl.GetChange(base_branch, None)
5225 return owners_finder.OwnersFinder(
5226 [f.LocalPath() for f in
5227 cl.GetChange(base_branch, None).AffectedFiles()],
5228 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005229 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005230 disable_color=options.no_color).run()
5231
5232
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005233def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005234 """Generates a diff command."""
5235 # Generate diff for the current branch's changes.
5236 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5237 upstream_commit, '--' ]
5238
5239 if args:
5240 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005241 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005242 diff_cmd.append(arg)
5243 else:
5244 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005245
5246 return diff_cmd
5247
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005248def MatchingFileType(file_name, extensions):
5249 """Returns true if the file name ends with one of the given extensions."""
5250 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005251
enne@chromium.org555cfe42014-01-29 18:21:39 +00005252@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005253def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005254 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005255 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005256 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005257 parser.add_option('--full', action='store_true',
5258 help='Reformat the full content of all touched files')
5259 parser.add_option('--dry-run', action='store_true',
5260 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005261 parser.add_option('--python', action='store_true',
5262 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005263 parser.add_option('--diff', action='store_true',
5264 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005265 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005266
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005267 # git diff generates paths against the root of the repository. Change
5268 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005269 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005270 if rel_base_path:
5271 os.chdir(rel_base_path)
5272
digit@chromium.org29e47272013-05-17 17:01:46 +00005273 # Grab the merge-base commit, i.e. the upstream commit of the current
5274 # branch when it was created or the last time it was rebased. This is
5275 # to cover the case where the user may have called "git fetch origin",
5276 # moving the origin branch to a newer commit, but hasn't rebased yet.
5277 upstream_commit = None
5278 cl = Changelist()
5279 upstream_branch = cl.GetUpstreamBranch()
5280 if upstream_branch:
5281 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5282 upstream_commit = upstream_commit.strip()
5283
5284 if not upstream_commit:
5285 DieWithError('Could not find base commit for this branch. '
5286 'Are you in detached state?')
5287
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005288 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5289 diff_output = RunGit(changed_files_cmd)
5290 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005291 # Filter out files deleted by this CL
5292 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005293
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005294 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5295 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5296 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005297 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005298
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005299 top_dir = os.path.normpath(
5300 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5301
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005302 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5303 # formatted. This is used to block during the presubmit.
5304 return_value = 0
5305
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005306 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005307 # Locate the clang-format binary in the checkout
5308 try:
5309 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005310 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005311 DieWithError(e)
5312
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005313 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005314 cmd = [clang_format_tool]
5315 if not opts.dry_run and not opts.diff:
5316 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005317 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005318 if opts.diff:
5319 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005320 else:
5321 env = os.environ.copy()
5322 env['PATH'] = str(os.path.dirname(clang_format_tool))
5323 try:
5324 script = clang_format.FindClangFormatScriptInChromiumTree(
5325 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005326 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005327 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005328
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005329 cmd = [sys.executable, script, '-p0']
5330 if not opts.dry_run and not opts.diff:
5331 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005332
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005333 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5334 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005335
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005336 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5337 if opts.diff:
5338 sys.stdout.write(stdout)
5339 if opts.dry_run and len(stdout) > 0:
5340 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005341
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005342 # Similar code to above, but using yapf on .py files rather than clang-format
5343 # on C/C++ files
5344 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005345 yapf_tool = gclient_utils.FindExecutable('yapf')
5346 if yapf_tool is None:
5347 DieWithError('yapf not found in PATH')
5348
5349 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005350 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005351 cmd = [yapf_tool]
5352 if not opts.dry_run and not opts.diff:
5353 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005354 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005355 if opts.diff:
5356 sys.stdout.write(stdout)
5357 else:
5358 # TODO(sbc): yapf --lines mode still has some issues.
5359 # https://github.com/google/yapf/issues/154
5360 DieWithError('--python currently only works with --full')
5361
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005362 # Dart's formatter does not have the nice property of only operating on
5363 # modified chunks, so hard code full.
5364 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005365 try:
5366 command = [dart_format.FindDartFmtToolInChromiumTree()]
5367 if not opts.dry_run and not opts.diff:
5368 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005369 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005370
ppi@chromium.org6593d932016-03-03 15:41:15 +00005371 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005372 if opts.dry_run and stdout:
5373 return_value = 2
5374 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005375 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5376 'found in this checkout. Files in other languages are still '
5377 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005378
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005379 # Format GN build files. Always run on full build files for canonical form.
5380 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005381 cmd = ['gn', 'format' ]
5382 if opts.dry_run or opts.diff:
5383 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005384 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005385 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5386 shell=sys.platform == 'win32',
5387 cwd=top_dir)
5388 if opts.dry_run and gn_ret == 2:
5389 return_value = 2 # Not formatted.
5390 elif opts.diff and gn_ret == 2:
5391 # TODO this should compute and print the actual diff.
5392 print("This change has GN build file diff for " + gn_diff_file)
5393 elif gn_ret != 0:
5394 # For non-dry run cases (and non-2 return values for dry-run), a
5395 # nonzero error code indicates a failure, probably because the file
5396 # doesn't parse.
5397 DieWithError("gn format failed on " + gn_diff_file +
5398 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005399
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005400 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005401
5402
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005403@subcommand.usage('<codereview url or issue id>')
5404def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005405 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005406 _, args = parser.parse_args(args)
5407
5408 if len(args) != 1:
5409 parser.print_help()
5410 return 1
5411
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005412 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005413 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005414 parser.print_help()
5415 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005416 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005417
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005418 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005419 output = RunGit(['config', '--local', '--get-regexp',
5420 r'branch\..*\.%s' % issueprefix],
5421 error_ok=True)
5422 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005423 if issue == target_issue:
5424 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005425
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005426 branches = []
5427 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005428 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005429 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005430 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005431 return 1
5432 if len(branches) == 1:
5433 RunGit(['checkout', branches[0]])
5434 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005435 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005436 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005437 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005438 which = raw_input('Choose by index: ')
5439 try:
5440 RunGit(['checkout', branches[int(which)]])
5441 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005442 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005443 return 1
5444
5445 return 0
5446
5447
maruel@chromium.org29404b52014-09-08 22:58:00 +00005448def CMDlol(parser, args):
5449 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005450 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005451 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5452 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5453 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005454 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005455 return 0
5456
5457
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005458class OptionParser(optparse.OptionParser):
5459 """Creates the option parse and add --verbose support."""
5460 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005461 optparse.OptionParser.__init__(
5462 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005463 self.add_option(
5464 '-v', '--verbose', action='count', default=0,
5465 help='Use 2 times for more debugging info')
5466
5467 def parse_args(self, args=None, values=None):
5468 options, args = optparse.OptionParser.parse_args(self, args, values)
5469 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5470 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5471 return options, args
5472
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005474def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005475 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005476 print('\nYour python version %s is unsupported, please upgrade.\n' %
5477 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005478 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005479
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005480 # Reload settings.
5481 global settings
5482 settings = Settings()
5483
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005484 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005485 dispatcher = subcommand.CommandDispatcher(__name__)
5486 try:
5487 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005488 except auth.AuthenticationError as e:
5489 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005490 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005491 if e.code != 500:
5492 raise
5493 DieWithError(
5494 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5495 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005496 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005497
5498
5499if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005500 # These affect sys.stdout so do it outside of main() to simplify mocks in
5501 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005502 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005503 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005504 try:
5505 sys.exit(main(sys.argv[1:]))
5506 except KeyboardInterrupt:
5507 sys.stderr.write('interrupted\n')
5508 sys.exit(1)