blob: 27dd8b4ed76b0240f40e2561e72a32b101a40ee6 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
skobes6468b902016-10-24 08:45:10 -070043import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
88
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070090 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091 sys.exit(1)
92
93
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000094def GetNoGitPagerEnv():
95 env = os.environ.copy()
96 # 'cat' is a magical git string that disables pagers on all platforms.
97 env['GIT_PAGER'] = 'cat'
98 return env
99
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000100
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000104 except subprocess2.CalledProcessError as e:
105 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 'Command "%s" failed.\n%s' % (
109 ' '.join(args), error_message or e.stdout or ''))
110 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
113def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000115 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000119 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 try:
tandrii5d48c322016-08-18 16:19:37 -0700125 (out, _), code = subprocess2.communicate(['git'] + args,
126 env=GetNoGitPagerEnv(),
127 stdout=subprocess2.PIPE,
128 stderr=stderr)
129 return code, out
130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
132 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
tandrii2a16b952016-10-19 07:09:44 -0700154def time_sleep(seconds):
155 # Use this so that it can be mocked in tests without interfering with python
156 # system machinery.
157 import time # Local import to discourage others from importing time globally.
158 return time.sleep(seconds)
159
160
maruel@chromium.org90541732011-04-01 17:54:18 +0000161def ask_for_data(prompt):
162 try:
163 return raw_input(prompt)
164 except KeyboardInterrupt:
165 # Hide the exception.
166 sys.exit(1)
167
168
tandrii5d48c322016-08-18 16:19:37 -0700169def _git_branch_config_key(branch, key):
170 """Helper method to return Git config key for a branch."""
171 assert branch, 'branch name is required to set git config for it'
172 return 'branch.%s.%s' % (branch, key)
173
174
175def _git_get_branch_config_value(key, default=None, value_type=str,
176 branch=False):
177 """Returns git config value of given or current branch if any.
178
179 Returns default in all other cases.
180 """
181 assert value_type in (int, str, bool)
182 if branch is False: # Distinguishing default arg value from None.
183 branch = GetCurrentBranch()
184
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000185 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700186 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187
tandrii5d48c322016-08-18 16:19:37 -0700188 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700189 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700190 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700191 # git config also has --int, but apparently git config suffers from integer
192 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700193 args.append(_git_branch_config_key(branch, key))
194 code, out = RunGitWithCode(args)
195 if code == 0:
196 value = out.strip()
197 if value_type == int:
198 return int(value)
199 if value_type == bool:
200 return bool(value.lower() == 'true')
201 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 return default
203
204
tandrii5d48c322016-08-18 16:19:37 -0700205def _git_set_branch_config_value(key, value, branch=None, **kwargs):
206 """Sets the value or unsets if it's None of a git branch config.
207
208 Valid, though not necessarily existing, branch must be provided,
209 otherwise currently checked out branch is used.
210 """
211 if not branch:
212 branch = GetCurrentBranch()
213 assert branch, 'a branch name OR currently checked out branch is required'
214 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700215 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700216 if value is None:
217 args.append('--unset')
218 elif isinstance(value, bool):
219 args.append('--bool')
220 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700221 else:
tandrii33a46ff2016-08-23 05:53:40 -0700222 # git config also has --int, but apparently git config suffers from integer
223 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700224 value = str(value)
225 args.append(_git_branch_config_key(branch, key))
226 if value is not None:
227 args.append(value)
228 RunGit(args, **kwargs)
229
230
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000231def add_git_similarity(parser):
232 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700233 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000234 help='Sets the percentage that a pair of files need to match in order to'
235 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000236 parser.add_option(
237 '--find-copies', action='store_true',
238 help='Allows git to look for copies.')
239 parser.add_option(
240 '--no-find-copies', action='store_false', dest='find_copies',
241 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000242
243 old_parser_args = parser.parse_args
244 def Parse(args):
245 options, args = old_parser_args(args)
246
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000247 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.similarity = _git_get_branch_config_value(
249 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 print('Note: Saving similarity of %d%% in git config.'
252 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700253 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 options.similarity = max(0, min(options.similarity, 100))
256
257 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700258 options.find_copies = _git_get_branch_config_value(
259 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000260 else:
tandrii5d48c322016-08-18 16:19:37 -0700261 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 print('Using %d%% similarity for rename/copy detection. '
264 'Override with --similarity.' % options.similarity)
265
266 return options, args
267 parser.parse_args = Parse
268
269
machenbach@chromium.org45453142015-09-15 08:45:22 +0000270def _get_properties_from_options(options):
271 properties = dict(x.split('=', 1) for x in options.properties)
272 for key, val in properties.iteritems():
273 try:
274 properties[key] = json.loads(val)
275 except ValueError:
276 pass # If a value couldn't be evaluated, treat it as a string.
277 return properties
278
279
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280def _prefix_master(master):
281 """Convert user-specified master name to full master name.
282
283 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
284 name, while the developers always use shortened master name
285 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
286 function does the conversion for buildbucket migration.
287 """
borenet6c0efe62016-10-19 08:13:29 -0700288 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 return master
borenet6c0efe62016-10-19 08:13:29 -0700290 return '%s%s' % (MASTER_PREFIX, master)
291
292
293def _unprefix_master(bucket):
294 """Convert bucket name to shortened master name.
295
296 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
297 name, while the developers always use shortened master name
298 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
299 function does the conversion for buildbucket migration.
300 """
301 if bucket.startswith(MASTER_PREFIX):
302 return bucket[len(MASTER_PREFIX):]
303 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306def _buildbucket_retry(operation_name, http, *args, **kwargs):
307 """Retries requests to buildbucket service and returns parsed json content."""
308 try_count = 0
309 while True:
310 response, content = http.request(*args, **kwargs)
311 try:
312 content_json = json.loads(content)
313 except ValueError:
314 content_json = None
315
316 # Buildbucket could return an error even if status==200.
317 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000318 error = content_json.get('error')
319 if error.get('code') == 403:
320 raise BuildbucketResponseException(
321 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000322 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000323 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000324 raise BuildbucketResponseException(msg)
325
326 if response.status == 200:
327 if not content_json:
328 raise BuildbucketResponseException(
329 'Buildbucket returns invalid json content: %s.\n'
330 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
331 content)
332 return content_json
333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content)
335
336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700338 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 try_count += 1
340 assert False, 'unreachable'
341
342
qyearsley1fdfcb62016-10-24 13:22:03 -0700343def _get_bucket_map(changelist, options, option_parser):
344 """Returns a dict mapping bucket names (or master names) to
345 builders and tests, for triggering try jobs.
346 """
347 if not options.bot:
348 change = changelist.GetChange(
349 changelist.GetCommonAncestorWithUpstream(), None)
350
351 # Get try masters from PRESUBMIT.py files.
352 masters = presubmit_support.DoGetTryMasters(
353 change=change,
354 changed_files=change.LocalPaths(),
355 repository_root=settings.GetRoot(),
356 default_presubmit=None,
357 project=None,
358 verbose=options.verbose,
359 output_stream=sys.stdout)
360
361 if masters:
362 return masters
363
364 # Fall back to deprecated method: get try slaves from PRESUBMIT.py
365 # files.
366 options.bot = presubmit_support.DoGetTrySlaves(
367 change=change,
368 changed_files=change.LocalPaths(),
369 repository_root=settings.GetRoot(),
370 default_presubmit=None,
371 project=None,
372 verbose=options.verbose,
373 output_stream=sys.stdout)
374
375 if not options.bot:
376 return {}
377
378 if options.bucket:
379 return {options.bucket: {b: [] for b in options.bot}}
380
381 builders_and_tests = {}
382
383 # TODO(machenbach): The old style command-line options don't support
384 # multiple try masters yet.
385 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
386 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
387
388 for bot in old_style:
389 if ':' in bot:
390 option_parser.error('Specifying testfilter is no longer supported')
391 elif ',' in bot:
392 option_parser.error('Specify one bot per --bot flag')
393 else:
394 builders_and_tests.setdefault(bot, [])
395
396 for bot, tests in new_style:
397 builders_and_tests.setdefault(bot, []).extend(tests)
398
399 if not options.master:
400 # TODO(qyearsley): crbug.com/640740
401 options.master, error_message = _get_builder_master(options.bot)
402 if error_message:
403 option_parser.error(
404 'Tryserver master cannot be found because: %s\n'
405 'Please manually specify the tryserver master, e.g. '
406 '"-m tryserver.chromium.linux".' % error_message)
407
408 # Return a master map with one master to be backwards compatible. The
409 # master name defaults to an empty string, which will cause the master
410 # not to be set on rietveld (deprecated).
411 bucket = ''
412 if options.master:
413 # Add the "master." prefix to the master name to obtain the bucket name.
414 bucket = _prefix_master(options.master)
415 return {bucket: builders_and_tests}
416
417
418def _get_builder_master(bot_list):
419 """Fetches a master for the given list of builders.
420
421 Returns a pair (master, error_message), where either master or
422 error_message is None.
423 """
424 map_url = 'https://builders-map.appspot.com/'
425 try:
426 master_map = json.load(urllib2.urlopen(map_url))
427 except urllib2.URLError as e:
428 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
429 (map_url, e))
430 except ValueError as e:
431 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
432 if not master_map:
433 return None, 'Failed to build master map.'
434
435 result_master = ''
436 for bot in bot_list:
437 builder = bot.split(':', 1)[0]
438 master_list = master_map.get(builder, [])
439 if not master_list:
440 return None, ('No matching master for builder %s.' % builder)
441 elif len(master_list) > 1:
442 return None, ('The builder name %s exists in multiple masters %s.' %
443 (builder, master_list))
444 else:
445 cur_master = master_list[0]
446 if not result_master:
447 result_master = cur_master
448 elif result_master != cur_master:
449 return None, 'The builders do not belong to the same master.'
450 return result_master, None
451
452
borenet6c0efe62016-10-19 08:13:29 -0700453def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700454 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700455 """Sends a request to Buildbucket to trigger try jobs for a changelist.
456
457 Args:
458 auth_config: AuthConfig for Rietveld.
459 changelist: Changelist that the try jobs are associated with.
460 buckets: A nested dict mapping bucket names to builders to tests.
461 options: Command-line options.
462 """
tandriide281ae2016-10-12 06:02:30 -0700463 assert changelist.GetIssue(), 'CL must be uploaded first'
464 codereview_url = changelist.GetCodereviewServer()
465 assert codereview_url, 'CL must be uploaded first'
466 patchset = patchset or changelist.GetMostRecentPatchset()
467 assert patchset, 'CL must be uploaded first'
468
469 codereview_host = urlparse.urlparse(codereview_url).hostname
470 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000471 http = authenticator.authorize(httplib2.Http())
472 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700473
474 # TODO(tandrii): consider caching Gerrit CL details just like
475 # _RietveldChangelistImpl does, then caching values in these two variables
476 # won't be necessary.
477 owner_email = changelist.GetIssueOwner()
478 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000479
480 buildbucket_put_url = (
481 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000482 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700483 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
484 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
485 hostname=codereview_host,
486 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700488 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489
490 batch_req_body = {'builds': []}
491 print_text = []
492 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700493 for bucket, builders_and_tests in sorted(buckets.iteritems()):
494 print_text.append('Bucket: %s' % bucket)
495 master = None
496 if bucket.startswith(MASTER_PREFIX):
497 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 for builder, tests in sorted(builders_and_tests.iteritems()):
499 print_text.append(' %s: %s' % (builder, tests))
500 parameters = {
501 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000502 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700503 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000504 'revision': options.revision,
505 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 'properties': {
507 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700508 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700509 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 'patch_storage': 'rietveld',
511 'patchset': patchset,
tandriide281ae2016-10-12 06:02:30 -0700512 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 },
514 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000515 if 'presubmit' in builder.lower():
516 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000517 if tests:
518 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700519 if extra_properties:
520 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521 if options.clobber:
522 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700523
524 tags = [
525 'builder:%s' % builder,
526 'buildset:%s' % buildset,
527 'user_agent:git_cl_try',
528 ]
529 if master:
530 parameters['properties']['master'] = master
531 tags.append('master:%s' % master)
532
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000533 batch_req_body['builds'].append(
534 {
535 'bucket': bucket,
536 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700538 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000539 }
540 )
541
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700543 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 http,
545 buildbucket_put_url,
546 'PUT',
547 body=json.dumps(batch_req_body),
548 headers={'Content-Type': 'application/json'}
549 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000550 print_text.append('To see results here, run: git cl try-results')
551 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700552 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000553
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000554
tandrii221ab252016-10-06 08:12:04 -0700555def fetch_try_jobs(auth_config, changelist, buildbucket_host,
556 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700557 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558
qyearsley53f48a12016-09-01 10:45:13 -0700559 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 """
tandrii221ab252016-10-06 08:12:04 -0700561 assert buildbucket_host
562 assert changelist.GetIssue(), 'CL must be uploaded first'
563 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
564 patchset = patchset or changelist.GetMostRecentPatchset()
565 assert patchset, 'CL must be uploaded first'
566
567 codereview_url = changelist.GetCodereviewServer()
568 codereview_host = urlparse.urlparse(codereview_url).hostname
569 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 if authenticator.has_cached_credentials():
571 http = authenticator.authorize(httplib2.Http())
572 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700573 print('Warning: Some results might be missing because %s' %
574 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700575 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 http = httplib2.Http()
577
578 http.force_exception_to_status_code = True
579
tandrii221ab252016-10-06 08:12:04 -0700580 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
581 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
582 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700584 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 params = {'tag': 'buildset:%s' % buildset}
586
587 builds = {}
588 while True:
589 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700590 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700592 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 for build in content.get('builds', []):
594 builds[build['id']] = build
595 if 'next_cursor' in content:
596 params['start_cursor'] = content['next_cursor']
597 else:
598 break
599 return builds
600
601
qyearsleyeab3c042016-08-24 09:18:28 -0700602def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000603 """Prints nicely result of fetch_try_jobs."""
604 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700605 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000606 return
607
608 # Make a copy, because we'll be modifying builds dictionary.
609 builds = builds.copy()
610 builder_names_cache = {}
611
612 def get_builder(b):
613 try:
614 return builder_names_cache[b['id']]
615 except KeyError:
616 try:
617 parameters = json.loads(b['parameters_json'])
618 name = parameters['builder_name']
619 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700620 print('WARNING: failed to get builder name for build %s: %s' % (
621 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000622 name = None
623 builder_names_cache[b['id']] = name
624 return name
625
626 def get_bucket(b):
627 bucket = b['bucket']
628 if bucket.startswith('master.'):
629 return bucket[len('master.'):]
630 return bucket
631
632 if options.print_master:
633 name_fmt = '%%-%ds %%-%ds' % (
634 max(len(str(get_bucket(b))) for b in builds.itervalues()),
635 max(len(str(get_builder(b))) for b in builds.itervalues()))
636 def get_name(b):
637 return name_fmt % (get_bucket(b), get_builder(b))
638 else:
639 name_fmt = '%%-%ds' % (
640 max(len(str(get_builder(b))) for b in builds.itervalues()))
641 def get_name(b):
642 return name_fmt % get_builder(b)
643
644 def sort_key(b):
645 return b['status'], b.get('result'), get_name(b), b.get('url')
646
647 def pop(title, f, color=None, **kwargs):
648 """Pop matching builds from `builds` dict and print them."""
649
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000650 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000651 colorize = str
652 else:
653 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
654
655 result = []
656 for b in builds.values():
657 if all(b.get(k) == v for k, v in kwargs.iteritems()):
658 builds.pop(b['id'])
659 result.append(b)
660 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700661 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000662 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700663 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000664
665 total = len(builds)
666 pop(status='COMPLETED', result='SUCCESS',
667 title='Successes:', color=Fore.GREEN,
668 f=lambda b: (get_name(b), b.get('url')))
669 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
670 title='Infra Failures:', color=Fore.MAGENTA,
671 f=lambda b: (get_name(b), b.get('url')))
672 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
673 title='Failures:', color=Fore.RED,
674 f=lambda b: (get_name(b), b.get('url')))
675 pop(status='COMPLETED', result='CANCELED',
676 title='Canceled:', color=Fore.MAGENTA,
677 f=lambda b: (get_name(b),))
678 pop(status='COMPLETED', result='FAILURE',
679 failure_reason='INVALID_BUILD_DEFINITION',
680 title='Wrong master/builder name:', color=Fore.MAGENTA,
681 f=lambda b: (get_name(b),))
682 pop(status='COMPLETED', result='FAILURE',
683 title='Other failures:',
684 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
685 pop(status='COMPLETED',
686 title='Other finished:',
687 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
688 pop(status='STARTED',
689 title='Started:', color=Fore.YELLOW,
690 f=lambda b: (get_name(b), b.get('url')))
691 pop(status='SCHEDULED',
692 title='Scheduled:',
693 f=lambda b: (get_name(b), 'id=%s' % b['id']))
694 # The last section is just in case buildbucket API changes OR there is a bug.
695 pop(title='Other:',
696 f=lambda b: (get_name(b), 'id=%s' % b['id']))
697 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700698 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000699
700
qyearsley53f48a12016-09-01 10:45:13 -0700701def write_try_results_json(output_file, builds):
702 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
703
704 The input |builds| dict is assumed to be generated by Buildbucket.
705 Buildbucket documentation: http://goo.gl/G0s101
706 """
707
708 def convert_build_dict(build):
709 return {
710 'buildbucket_id': build.get('id'),
711 'status': build.get('status'),
712 'result': build.get('result'),
713 'bucket': build.get('bucket'),
714 'builder_name': json.loads(
715 build.get('parameters_json', '{}')).get('builder_name'),
716 'failure_reason': build.get('failure_reason'),
717 'url': build.get('url'),
718 }
719
720 converted = []
721 for _, build in sorted(builds.items()):
722 converted.append(convert_build_dict(build))
723 write_json(output_file, converted)
724
725
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000726def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
727 """Return the corresponding git ref if |base_url| together with |glob_spec|
728 matches the full |url|.
729
730 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
731 """
732 fetch_suburl, as_ref = glob_spec.split(':')
733 if allow_wildcards:
734 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
735 if glob_match:
736 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
737 # "branches/{472,597,648}/src:refs/remotes/svn/*".
738 branch_re = re.escape(base_url)
739 if glob_match.group(1):
740 branch_re += '/' + re.escape(glob_match.group(1))
741 wildcard = glob_match.group(2)
742 if wildcard == '*':
743 branch_re += '([^/]*)'
744 else:
745 # Escape and replace surrounding braces with parentheses and commas
746 # with pipe symbols.
747 wildcard = re.escape(wildcard)
748 wildcard = re.sub('^\\\\{', '(', wildcard)
749 wildcard = re.sub('\\\\,', '|', wildcard)
750 wildcard = re.sub('\\\\}$', ')', wildcard)
751 branch_re += wildcard
752 if glob_match.group(3):
753 branch_re += re.escape(glob_match.group(3))
754 match = re.match(branch_re, url)
755 if match:
756 return re.sub('\*$', match.group(1), as_ref)
757
758 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
759 if fetch_suburl:
760 full_url = base_url + '/' + fetch_suburl
761 else:
762 full_url = base_url
763 if full_url == url:
764 return as_ref
765 return None
766
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000767
iannucci@chromium.org79540052012-10-19 23:15:26 +0000768def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000769 """Prints statistics about the change to the user."""
770 # --no-ext-diff is broken in some versions of Git, so try to work around
771 # this by overriding the environment (but there is still a problem if the
772 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000773 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000774 if 'GIT_EXTERNAL_DIFF' in env:
775 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000776
777 if find_copies:
778 similarity_options = ['--find-copies-harder', '-l100000',
779 '-C%s' % similarity]
780 else:
781 similarity_options = ['-M%s' % similarity]
782
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000783 try:
784 stdout = sys.stdout.fileno()
785 except AttributeError:
786 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000787 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000788 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000789 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000790 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000791
792
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000793class BuildbucketResponseException(Exception):
794 pass
795
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797class Settings(object):
798 def __init__(self):
799 self.default_server = None
800 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000801 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 self.is_git_svn = None
803 self.svn_branch = None
804 self.tree_status_url = None
805 self.viewvc_url = None
806 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000807 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000808 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000809 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000810 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000811 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000812 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000813 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700814 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815
816 def LazyUpdateIfNeeded(self):
817 """Updates the settings from a codereview.settings file, if available."""
818 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000819 # The only value that actually changes the behavior is
820 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000821 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000822 error_ok=True
823 ).strip().lower()
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000826 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 LoadCodereviewSettingsFromFile(cr_settings_file)
828 self.updated = True
829
830 def GetDefaultServerUrl(self, error_ok=False):
831 if not self.default_server:
832 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000833 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000834 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835 if error_ok:
836 return self.default_server
837 if not self.default_server:
838 error_message = ('Could not find settings file. You must configure '
839 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000840 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000841 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 return self.default_server
843
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000844 @staticmethod
845 def GetRelativeRoot():
846 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000849 if self.root is None:
850 self.root = os.path.abspath(self.GetRelativeRoot())
851 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000853 def GetGitMirror(self, remote='origin'):
854 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000855 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000856 if not os.path.isdir(local_url):
857 return None
858 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
859 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
860 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
861 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
862 if mirror.exists():
863 return mirror
864 return None
865
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866 def GetIsGitSvn(self):
867 """Return true if this repo looks like it's using git-svn."""
868 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000869 if self.GetPendingRefPrefix():
870 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
871 self.is_git_svn = False
872 else:
873 # If you have any "svn-remote.*" config keys, we think you're using svn.
874 self.is_git_svn = RunGitWithCode(
875 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 return self.is_git_svn
877
878 def GetSVNBranch(self):
879 if self.svn_branch is None:
880 if not self.GetIsGitSvn():
881 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
882
883 # Try to figure out which remote branch we're based on.
884 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000885 # 1) iterate through our branch history and find the svn URL.
886 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887
888 # regexp matching the git-svn line that contains the URL.
889 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
890
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000891 # We don't want to go through all of history, so read a line from the
892 # pipe at a time.
893 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000894 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000895 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
896 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000897 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000898 for line in proc.stdout:
899 match = git_svn_re.match(line)
900 if match:
901 url = match.group(1)
902 proc.stdout.close() # Cut pipe.
903 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000905 if url:
906 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
907 remotes = RunGit(['config', '--get-regexp',
908 r'^svn-remote\..*\.url']).splitlines()
909 for remote in remotes:
910 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000912 remote = match.group(1)
913 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000914 rewrite_root = RunGit(
915 ['config', 'svn-remote.%s.rewriteRoot' % remote],
916 error_ok=True).strip()
917 if rewrite_root:
918 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000919 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000920 ['config', 'svn-remote.%s.fetch' % remote],
921 error_ok=True).strip()
922 if fetch_spec:
923 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
924 if self.svn_branch:
925 break
926 branch_spec = RunGit(
927 ['config', 'svn-remote.%s.branches' % remote],
928 error_ok=True).strip()
929 if branch_spec:
930 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
931 if self.svn_branch:
932 break
933 tag_spec = RunGit(
934 ['config', 'svn-remote.%s.tags' % remote],
935 error_ok=True).strip()
936 if tag_spec:
937 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
938 if self.svn_branch:
939 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940
941 if not self.svn_branch:
942 DieWithError('Can\'t guess svn branch -- try specifying it on the '
943 'command line')
944
945 return self.svn_branch
946
947 def GetTreeStatusUrl(self, error_ok=False):
948 if not self.tree_status_url:
949 error_message = ('You must configure your tree status URL by running '
950 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000951 self.tree_status_url = self._GetRietveldConfig(
952 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 return self.tree_status_url
954
955 def GetViewVCUrl(self):
956 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000957 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958 return self.viewvc_url
959
rmistry@google.com90752582014-01-14 21:04:50 +0000960 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000961 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000962
rmistry@google.com78948ed2015-07-08 23:09:57 +0000963 def GetIsSkipDependencyUpload(self, branch_name):
964 """Returns true if specified branch should skip dep uploads."""
965 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
966 error_ok=True)
967
rmistry@google.com5626a922015-02-26 14:03:30 +0000968 def GetRunPostUploadHook(self):
969 run_post_upload_hook = self._GetRietveldConfig(
970 'run-post-upload-hook', error_ok=True)
971 return run_post_upload_hook == "True"
972
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000973 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000974 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000975
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000976 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000977 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000978
ukai@chromium.orge8077812012-02-03 03:41:46 +0000979 def GetIsGerrit(self):
980 """Return true if this repo is assosiated with gerrit code review system."""
981 if self.is_gerrit is None:
982 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
983 return self.is_gerrit
984
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000985 def GetSquashGerritUploads(self):
986 """Return true if uploads to Gerrit should be squashed by default."""
987 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700988 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
989 if self.squash_gerrit_uploads is None:
990 # Default is squash now (http://crbug.com/611892#c23).
991 self.squash_gerrit_uploads = not (
992 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
993 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000994 return self.squash_gerrit_uploads
995
tandriia60502f2016-06-20 02:01:53 -0700996 def GetSquashGerritUploadsOverride(self):
997 """Return True or False if codereview.settings should be overridden.
998
999 Returns None if no override has been defined.
1000 """
1001 # See also http://crbug.com/611892#c23
1002 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
1003 error_ok=True).strip()
1004 if result == 'true':
1005 return True
1006 if result == 'false':
1007 return False
1008 return None
1009
tandrii@chromium.org28253532016-04-14 13:46:56 +00001010 def GetGerritSkipEnsureAuthenticated(self):
1011 """Return True if EnsureAuthenticated should not be done for Gerrit
1012 uploads."""
1013 if self.gerrit_skip_ensure_authenticated is None:
1014 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00001015 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +00001016 error_ok=True).strip() == 'true')
1017 return self.gerrit_skip_ensure_authenticated
1018
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001019 def GetGitEditor(self):
1020 """Return the editor specified in the git config, or None if none is."""
1021 if self.git_editor is None:
1022 self.git_editor = self._GetConfig('core.editor', error_ok=True)
1023 return self.git_editor or None
1024
thestig@chromium.org44202a22014-03-11 19:22:18 +00001025 def GetLintRegex(self):
1026 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
1027 DEFAULT_LINT_REGEX)
1028
1029 def GetLintIgnoreRegex(self):
1030 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
1031 DEFAULT_LINT_IGNORE_REGEX)
1032
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001033 def GetProject(self):
1034 if not self.project:
1035 self.project = self._GetRietveldConfig('project', error_ok=True)
1036 return self.project
1037
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001038 def GetForceHttpsCommitUrl(self):
1039 if not self.force_https_commit_url:
1040 self.force_https_commit_url = self._GetRietveldConfig(
1041 'force-https-commit-url', error_ok=True)
1042 return self.force_https_commit_url
1043
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001044 def GetPendingRefPrefix(self):
1045 if not self.pending_ref_prefix:
1046 self.pending_ref_prefix = self._GetRietveldConfig(
1047 'pending-ref-prefix', error_ok=True)
1048 return self.pending_ref_prefix
1049
tandriif46c20f2016-09-14 06:17:05 -07001050 def GetHasGitNumberFooter(self):
1051 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1052 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1053 if not self.git_number_footer:
1054 self.git_number_footer = self._GetRietveldConfig(
1055 'git-number-footer', error_ok=True)
1056 return self.git_number_footer
1057
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001058 def _GetRietveldConfig(self, param, **kwargs):
1059 return self._GetConfig('rietveld.' + param, **kwargs)
1060
rmistry@google.com78948ed2015-07-08 23:09:57 +00001061 def _GetBranchConfig(self, branch_name, param, **kwargs):
1062 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 def _GetConfig(self, param, **kwargs):
1065 self.LazyUpdateIfNeeded()
1066 return RunGit(['config', param], **kwargs).strip()
1067
1068
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069def ShortBranchName(branch):
1070 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001071 return branch.replace('refs/heads/', '', 1)
1072
1073
1074def GetCurrentBranchRef():
1075 """Returns branch ref (e.g., refs/heads/master) or None."""
1076 return RunGit(['symbolic-ref', 'HEAD'],
1077 stderr=subprocess2.VOID, error_ok=True).strip() or None
1078
1079
1080def GetCurrentBranch():
1081 """Returns current branch or None.
1082
1083 For refs/heads/* branches, returns just last part. For others, full ref.
1084 """
1085 branchref = GetCurrentBranchRef()
1086 if branchref:
1087 return ShortBranchName(branchref)
1088 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089
1090
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001091class _CQState(object):
1092 """Enum for states of CL with respect to Commit Queue."""
1093 NONE = 'none'
1094 DRY_RUN = 'dry_run'
1095 COMMIT = 'commit'
1096
1097 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1098
1099
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001100class _ParsedIssueNumberArgument(object):
1101 def __init__(self, issue=None, patchset=None, hostname=None):
1102 self.issue = issue
1103 self.patchset = patchset
1104 self.hostname = hostname
1105
1106 @property
1107 def valid(self):
1108 return self.issue is not None
1109
1110
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001111def ParseIssueNumberArgument(arg):
1112 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1113 fail_result = _ParsedIssueNumberArgument()
1114
1115 if arg.isdigit():
1116 return _ParsedIssueNumberArgument(issue=int(arg))
1117 if not arg.startswith('http'):
1118 return fail_result
1119 url = gclient_utils.UpgradeToHttps(arg)
1120 try:
1121 parsed_url = urlparse.urlparse(url)
1122 except ValueError:
1123 return fail_result
1124 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1125 tmp = cls.ParseIssueURL(parsed_url)
1126 if tmp is not None:
1127 return tmp
1128 return fail_result
1129
1130
tandriic2405f52016-10-10 08:13:15 -07001131class GerritIssueNotExists(Exception):
1132 def __init__(self, issue, url):
1133 self.issue = issue
1134 self.url = url
1135 super(GerritIssueNotExists, self).__init__()
1136
1137 def __str__(self):
1138 return 'issue %s at %s does not exist or you have no access to it' % (
1139 self.issue, self.url)
1140
1141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 """Changelist works with one changelist in local branch.
1144
1145 Supports two codereview backends: Rietveld or Gerrit, selected at object
1146 creation.
1147
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001148 Notes:
1149 * Not safe for concurrent multi-{thread,process} use.
1150 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001151 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001152 """
1153
1154 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1155 """Create a new ChangeList instance.
1156
1157 If issue is given, the codereview must be given too.
1158
1159 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1160 Otherwise, it's decided based on current configuration of the local branch,
1161 with default being 'rietveld' for backwards compatibility.
1162 See _load_codereview_impl for more details.
1163
1164 **kwargs will be passed directly to codereview implementation.
1165 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001167 global settings
1168 if not settings:
1169 # Happens when git_cl.py is used as a utility library.
1170 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001171
1172 if issue:
1173 assert codereview, 'codereview must be known, if issue is known'
1174
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 self.branchref = branchref
1176 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001177 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 self.branch = ShortBranchName(self.branchref)
1179 else:
1180 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001182 self.lookedup_issue = False
1183 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 self.has_description = False
1185 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001186 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001188 self.cc = None
1189 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001190 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001191
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001192 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001193 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001194 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001195 assert self._codereview_impl
1196 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001197
1198 def _load_codereview_impl(self, codereview=None, **kwargs):
1199 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001200 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1201 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1202 self._codereview = codereview
1203 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001204 return
1205
1206 # Automatic selection based on issue number set for a current branch.
1207 # Rietveld takes precedence over Gerrit.
1208 assert not self.issue
1209 # Whether we find issue or not, we are doing the lookup.
1210 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001211 if self.GetBranch():
1212 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1213 issue = _git_get_branch_config_value(
1214 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1215 if issue:
1216 self._codereview = codereview
1217 self._codereview_impl = cls(self, **kwargs)
1218 self.issue = int(issue)
1219 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220
1221 # No issue is set for this branch, so decide based on repo-wide settings.
1222 return self._load_codereview_impl(
1223 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1224 **kwargs)
1225
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001226 def IsGerrit(self):
1227 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001228
1229 def GetCCList(self):
1230 """Return the users cc'd on this CL.
1231
agable92bec4f2016-08-24 09:27:27 -07001232 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001233 """
1234 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001235 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001236 more_cc = ','.join(self.watchers)
1237 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1238 return self.cc
1239
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001240 def GetCCListWithoutDefault(self):
1241 """Return the users cc'd on this CL excluding default ones."""
1242 if self.cc is None:
1243 self.cc = ','.join(self.watchers)
1244 return self.cc
1245
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001246 def SetWatchers(self, watchers):
1247 """Set the list of email addresses that should be cc'd based on the changed
1248 files in this CL.
1249 """
1250 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251
1252 def GetBranch(self):
1253 """Returns the short branch name, e.g. 'master'."""
1254 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001255 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001256 if not branchref:
1257 return None
1258 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 self.branch = ShortBranchName(self.branchref)
1260 return self.branch
1261
1262 def GetBranchRef(self):
1263 """Returns the full branch name, e.g. 'refs/heads/master'."""
1264 self.GetBranch() # Poke the lazy loader.
1265 return self.branchref
1266
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001267 def ClearBranch(self):
1268 """Clears cached branch data of this object."""
1269 self.branch = self.branchref = None
1270
tandrii5d48c322016-08-18 16:19:37 -07001271 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1272 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1273 kwargs['branch'] = self.GetBranch()
1274 return _git_get_branch_config_value(key, default, **kwargs)
1275
1276 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1277 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1278 assert self.GetBranch(), (
1279 'this CL must have an associated branch to %sset %s%s' %
1280 ('un' if value is None else '',
1281 key,
1282 '' if value is None else ' to %r' % value))
1283 kwargs['branch'] = self.GetBranch()
1284 return _git_set_branch_config_value(key, value, **kwargs)
1285
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 @staticmethod
1287 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001288 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 e.g. 'origin', 'refs/heads/master'
1290 """
1291 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001292 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001295 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001297 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1298 error_ok=True).strip()
1299 if upstream_branch:
1300 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001302 # Fall back on trying a git-svn upstream branch.
1303 if settings.GetIsGitSvn():
1304 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001306 # Else, try to guess the origin remote.
1307 remote_branches = RunGit(['branch', '-r']).split()
1308 if 'origin/master' in remote_branches:
1309 # Fall back on origin/master if it exits.
1310 remote = 'origin'
1311 upstream_branch = 'refs/heads/master'
1312 elif 'origin/trunk' in remote_branches:
1313 # Fall back on origin/trunk if it exists. Generally a shared
1314 # git-svn clone
1315 remote = 'origin'
1316 upstream_branch = 'refs/heads/trunk'
1317 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001318 DieWithError(
1319 'Unable to determine default branch to diff against.\n'
1320 'Either pass complete "git diff"-style arguments, like\n'
1321 ' git cl upload origin/master\n'
1322 'or verify this branch is set up to track another \n'
1323 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
1325 return remote, upstream_branch
1326
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001327 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001328 upstream_branch = self.GetUpstreamBranch()
1329 if not BranchExists(upstream_branch):
1330 DieWithError('The upstream for the current branch (%s) does not exist '
1331 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001332 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001333 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001334
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 def GetUpstreamBranch(self):
1336 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001339 upstream_branch = upstream_branch.replace('refs/heads/',
1340 'refs/remotes/%s/' % remote)
1341 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1342 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 self.upstream_branch = upstream_branch
1344 return self.upstream_branch
1345
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001347 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001348 remote, branch = None, self.GetBranch()
1349 seen_branches = set()
1350 while branch not in seen_branches:
1351 seen_branches.add(branch)
1352 remote, branch = self.FetchUpstreamTuple(branch)
1353 branch = ShortBranchName(branch)
1354 if remote != '.' or branch.startswith('refs/remotes'):
1355 break
1356 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001357 remotes = RunGit(['remote'], error_ok=True).split()
1358 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001359 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001360 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001361 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001362 logging.warning('Could not determine which remote this change is '
1363 'associated with, so defaulting to "%s". This may '
1364 'not be what you want. You may prevent this message '
1365 'by running "git svn info" as documented here: %s',
1366 self._remote,
1367 GIT_INSTRUCTIONS_URL)
1368 else:
1369 logging.warn('Could not determine which remote this change is '
1370 'associated with. You may prevent this message by '
1371 'running "git svn info" as documented here: %s',
1372 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001373 branch = 'HEAD'
1374 if branch.startswith('refs/remotes'):
1375 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001376 elif branch.startswith('refs/branch-heads/'):
1377 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001378 else:
1379 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001380 return self._remote
1381
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001382 def GitSanityChecks(self, upstream_git_obj):
1383 """Checks git repo status and ensures diff is from local commits."""
1384
sbc@chromium.org79706062015-01-14 21:18:12 +00001385 if upstream_git_obj is None:
1386 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001387 print('ERROR: unable to determine current branch (detached HEAD?)',
1388 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001389 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001390 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001391 return False
1392
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001393 # Verify the commit we're diffing against is in our current branch.
1394 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1395 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1396 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001397 print('ERROR: %s is not in the current branch. You may need to rebase '
1398 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001399 return False
1400
1401 # List the commits inside the diff, and verify they are all local.
1402 commits_in_diff = RunGit(
1403 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1404 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1405 remote_branch = remote_branch.strip()
1406 if code != 0:
1407 _, remote_branch = self.GetRemoteBranch()
1408
1409 commits_in_remote = RunGit(
1410 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1411
1412 common_commits = set(commits_in_diff) & set(commits_in_remote)
1413 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001414 print('ERROR: Your diff contains %d commits already in %s.\n'
1415 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1416 'the diff. If you are using a custom git flow, you can override'
1417 ' the reference used for this check with "git config '
1418 'gitcl.remotebranch <git-ref>".' % (
1419 len(common_commits), remote_branch, upstream_git_obj),
1420 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001421 return False
1422 return True
1423
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001424 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001425 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001426
1427 Returns None if it is not set.
1428 """
tandrii5d48c322016-08-18 16:19:37 -07001429 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001430
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001431 def GetGitSvnRemoteUrl(self):
1432 """Return the configured git-svn remote URL parsed from git svn info.
1433
1434 Returns None if it is not set.
1435 """
1436 # URL is dependent on the current directory.
1437 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1438 if data:
1439 keys = dict(line.split(': ', 1) for line in data.splitlines()
1440 if ': ' in line)
1441 return keys.get('URL', None)
1442 return None
1443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 def GetRemoteUrl(self):
1445 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1446
1447 Returns None if there is no remote.
1448 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001450 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1451
1452 # If URL is pointing to a local directory, it is probably a git cache.
1453 if os.path.isdir(url):
1454 url = RunGit(['config', 'remote.%s.url' % remote],
1455 error_ok=True,
1456 cwd=url).strip()
1457 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001459 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001460 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001461 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001462 self.issue = self._GitGetBranchConfigValue(
1463 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001464 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 return self.issue
1466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 def GetIssueURL(self):
1468 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001469 issue = self.GetIssue()
1470 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001471 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001472 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
1474 def GetDescription(self, pretty=False):
1475 if not self.has_description:
1476 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001477 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 self.has_description = True
1479 if pretty:
1480 wrapper = textwrap.TextWrapper()
1481 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1482 return wrapper.fill(self.description)
1483 return self.description
1484
1485 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001486 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001487 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001488 self.patchset = self._GitGetBranchConfigValue(
1489 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001490 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491 return self.patchset
1492
1493 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001494 """Set this branch's patchset. If patchset=0, clears the patchset."""
1495 assert self.GetBranch()
1496 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001497 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001498 else:
1499 self.patchset = int(patchset)
1500 self._GitSetBranchConfigValue(
1501 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001503 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001504 """Set this branch's issue. If issue isn't given, clears the issue."""
1505 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001507 issue = int(issue)
1508 self._GitSetBranchConfigValue(
1509 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001510 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001511 codereview_server = self._codereview_impl.GetCodereviewServer()
1512 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001513 self._GitSetBranchConfigValue(
1514 self._codereview_impl.CodereviewServerConfigKey(),
1515 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 else:
tandrii5d48c322016-08-18 16:19:37 -07001517 # Reset all of these just to be clean.
1518 reset_suffixes = [
1519 'last-upload-hash',
1520 self._codereview_impl.IssueConfigKey(),
1521 self._codereview_impl.PatchsetConfigKey(),
1522 self._codereview_impl.CodereviewServerConfigKey(),
1523 ] + self._PostUnsetIssueProperties()
1524 for prop in reset_suffixes:
1525 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001526 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001527 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528
dnjba1b0f32016-09-02 12:37:42 -07001529 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001530 if not self.GitSanityChecks(upstream_branch):
1531 DieWithError('\nGit sanity check failure')
1532
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001533 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001534 if not root:
1535 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001536 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001537
1538 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001539 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001540 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001541 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001542 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001543 except subprocess2.CalledProcessError:
1544 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001545 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001546 'This branch probably doesn\'t exist anymore. To reset the\n'
1547 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001548 ' git branch --set-upstream-to origin/master %s\n'
1549 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001550 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001551
maruel@chromium.org52424302012-08-29 15:14:30 +00001552 issue = self.GetIssue()
1553 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001554 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001555 description = self.GetDescription()
1556 else:
1557 # If the change was never uploaded, use the log messages of all commits
1558 # up to the branch point, as git cl upload will prefill the description
1559 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001560 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1561 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001562
1563 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001564 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001565 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001566 name,
1567 description,
1568 absroot,
1569 files,
1570 issue,
1571 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001572 author,
1573 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001574
dsansomee2d6fd92016-09-08 00:10:47 -07001575 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001576 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001577 return self._codereview_impl.UpdateDescriptionRemote(
1578 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579
1580 def RunHook(self, committing, may_prompt, verbose, change):
1581 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1582 try:
1583 return presubmit_support.DoPresubmitChecks(change, committing,
1584 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1585 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001586 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1587 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001588 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001589 DieWithError(
1590 ('%s\nMaybe your depot_tools is out of date?\n'
1591 'If all fails, contact maruel@') % e)
1592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1594 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001595 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1596 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001597 else:
1598 # Assume url.
1599 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1600 urlparse.urlparse(issue_arg))
1601 if not parsed_issue_arg or not parsed_issue_arg.valid:
1602 DieWithError('Failed to parse issue argument "%s". '
1603 'Must be an issue number or a valid URL.' % issue_arg)
1604 return self._codereview_impl.CMDPatchWithParsedIssue(
1605 parsed_issue_arg, reject, nocommit, directory)
1606
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001607 def CMDUpload(self, options, git_diff_args, orig_args):
1608 """Uploads a change to codereview."""
1609 if git_diff_args:
1610 # TODO(ukai): is it ok for gerrit case?
1611 base_branch = git_diff_args[0]
1612 else:
1613 if self.GetBranch() is None:
1614 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1615
1616 # Default to diffing against common ancestor of upstream branch
1617 base_branch = self.GetCommonAncestorWithUpstream()
1618 git_diff_args = [base_branch, 'HEAD']
1619
1620 # Make sure authenticated to codereview before running potentially expensive
1621 # hooks. It is a fast, best efforts check. Codereview still can reject the
1622 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001623 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001624
1625 # Apply watchlists on upload.
1626 change = self.GetChange(base_branch, None)
1627 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1628 files = [f.LocalPath() for f in change.AffectedFiles()]
1629 if not options.bypass_watchlists:
1630 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1631
1632 if not options.bypass_hooks:
1633 if options.reviewers or options.tbr_owners:
1634 # Set the reviewer list now so that presubmit checks can access it.
1635 change_description = ChangeDescription(change.FullDescriptionText())
1636 change_description.update_reviewers(options.reviewers,
1637 options.tbr_owners,
1638 change)
1639 change.SetDescriptionText(change_description.description)
1640 hook_results = self.RunHook(committing=False,
1641 may_prompt=not options.force,
1642 verbose=options.verbose,
1643 change=change)
1644 if not hook_results.should_continue():
1645 return 1
1646 if not options.reviewers and hook_results.reviewers:
1647 options.reviewers = hook_results.reviewers.split(',')
1648
1649 if self.GetIssue():
1650 latest_patchset = self.GetMostRecentPatchset()
1651 local_patchset = self.GetPatchset()
1652 if (latest_patchset and local_patchset and
1653 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001654 print('The last upload made from this repository was patchset #%d but '
1655 'the most recent patchset on the server is #%d.'
1656 % (local_patchset, latest_patchset))
1657 print('Uploading will still work, but if you\'ve uploaded to this '
1658 'issue from another machine or branch the patch you\'re '
1659 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001660 ask_for_data('About to upload; enter to confirm.')
1661
1662 print_stats(options.similarity, options.find_copies, git_diff_args)
1663 ret = self.CMDUploadChange(options, git_diff_args, change)
1664 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001665 if options.use_commit_queue:
1666 self.SetCQState(_CQState.COMMIT)
1667 elif options.cq_dry_run:
1668 self.SetCQState(_CQState.DRY_RUN)
1669
tandrii5d48c322016-08-18 16:19:37 -07001670 _git_set_branch_config_value('last-upload-hash',
1671 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001672 # Run post upload hooks, if specified.
1673 if settings.GetRunPostUploadHook():
1674 presubmit_support.DoPostUploadExecuter(
1675 change,
1676 self,
1677 settings.GetRoot(),
1678 options.verbose,
1679 sys.stdout)
1680
1681 # Upload all dependencies if specified.
1682 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001683 print()
1684 print('--dependencies has been specified.')
1685 print('All dependent local branches will be re-uploaded.')
1686 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001687 # Remove the dependencies flag from args so that we do not end up in a
1688 # loop.
1689 orig_args.remove('--dependencies')
1690 ret = upload_branch_deps(self, orig_args)
1691 return ret
1692
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001693 def SetCQState(self, new_state):
1694 """Update the CQ state for latest patchset.
1695
1696 Issue must have been already uploaded and known.
1697 """
1698 assert new_state in _CQState.ALL_STATES
1699 assert self.GetIssue()
1700 return self._codereview_impl.SetCQState(new_state)
1701
qyearsley1fdfcb62016-10-24 13:22:03 -07001702 def TriggerDryRun(self):
1703 """Triggers a dry run and prints a warning on failure."""
1704 # TODO(qyearsley): Either re-use this method in CMDset_commit
1705 # and CMDupload, or change CMDtry to trigger dry runs with
1706 # just SetCQState, and catch keyboard interrupt and other
1707 # errors in that method.
1708 try:
1709 self.SetCQState(_CQState.DRY_RUN)
1710 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1711 return 0
1712 except KeyboardInterrupt:
1713 raise
1714 except:
1715 print('WARNING: failed to trigger CQ Dry Run.\n'
1716 'Either:\n'
1717 ' * your project has no CQ\n'
1718 ' * you don\'t have permission to trigger Dry Run\n'
1719 ' * bug in this code (see stack trace below).\n'
1720 'Consider specifying which bots to trigger manually '
1721 'or asking your project owners for permissions '
1722 'or contacting Chrome Infrastructure team at '
1723 'https://www.chromium.org/infra\n\n')
1724 # Still raise exception so that stack trace is printed.
1725 raise
1726
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727 # Forward methods to codereview specific implementation.
1728
1729 def CloseIssue(self):
1730 return self._codereview_impl.CloseIssue()
1731
1732 def GetStatus(self):
1733 return self._codereview_impl.GetStatus()
1734
1735 def GetCodereviewServer(self):
1736 return self._codereview_impl.GetCodereviewServer()
1737
tandriide281ae2016-10-12 06:02:30 -07001738 def GetIssueOwner(self):
1739 """Get owner from codereview, which may differ from this checkout."""
1740 return self._codereview_impl.GetIssueOwner()
1741
1742 def GetIssueProject(self):
1743 """Get project from codereview, which may differ from what this
1744 checkout's codereview.settings or gerrit project URL say.
1745 """
1746 return self._codereview_impl.GetIssueProject()
1747
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 def GetApprovingReviewers(self):
1749 return self._codereview_impl.GetApprovingReviewers()
1750
1751 def GetMostRecentPatchset(self):
1752 return self._codereview_impl.GetMostRecentPatchset()
1753
tandriide281ae2016-10-12 06:02:30 -07001754 def CannotTriggerTryJobReason(self):
1755 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1756 return self._codereview_impl.CannotTriggerTryJobReason()
1757
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 def __getattr__(self, attr):
1759 # This is because lots of untested code accesses Rietveld-specific stuff
1760 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001761 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001762 # Note that child method defines __getattr__ as well, and forwards it here,
1763 # because _RietveldChangelistImpl is not cleaned up yet, and given
1764 # deprecation of Rietveld, it should probably be just removed.
1765 # Until that time, avoid infinite recursion by bypassing __getattr__
1766 # of implementation class.
1767 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001768
1769
1770class _ChangelistCodereviewBase(object):
1771 """Abstract base class encapsulating codereview specifics of a changelist."""
1772 def __init__(self, changelist):
1773 self._changelist = changelist # instance of Changelist
1774
1775 def __getattr__(self, attr):
1776 # Forward methods to changelist.
1777 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1778 # _RietveldChangelistImpl to avoid this hack?
1779 return getattr(self._changelist, attr)
1780
1781 def GetStatus(self):
1782 """Apply a rough heuristic to give a simple summary of an issue's review
1783 or CQ status, assuming adherence to a common workflow.
1784
1785 Returns None if no issue for this branch, or specific string keywords.
1786 """
1787 raise NotImplementedError()
1788
1789 def GetCodereviewServer(self):
1790 """Returns server URL without end slash, like "https://codereview.com"."""
1791 raise NotImplementedError()
1792
1793 def FetchDescription(self):
1794 """Fetches and returns description from the codereview server."""
1795 raise NotImplementedError()
1796
tandrii5d48c322016-08-18 16:19:37 -07001797 @classmethod
1798 def IssueConfigKey(cls):
1799 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001800 raise NotImplementedError()
1801
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001802 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001803 def PatchsetConfigKey(cls):
1804 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 raise NotImplementedError()
1806
tandrii5d48c322016-08-18 16:19:37 -07001807 @classmethod
1808 def CodereviewServerConfigKey(cls):
1809 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 raise NotImplementedError()
1811
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001812 def _PostUnsetIssueProperties(self):
1813 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001814 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001815
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001816 def GetRieveldObjForPresubmit(self):
1817 # This is an unfortunate Rietveld-embeddedness in presubmit.
1818 # For non-Rietveld codereviews, this probably should return a dummy object.
1819 raise NotImplementedError()
1820
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001821 def GetGerritObjForPresubmit(self):
1822 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1823 return None
1824
dsansomee2d6fd92016-09-08 00:10:47 -07001825 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001826 """Update the description on codereview site."""
1827 raise NotImplementedError()
1828
1829 def CloseIssue(self):
1830 """Closes the issue."""
1831 raise NotImplementedError()
1832
1833 def GetApprovingReviewers(self):
1834 """Returns a list of reviewers approving the change.
1835
1836 Note: not necessarily committers.
1837 """
1838 raise NotImplementedError()
1839
1840 def GetMostRecentPatchset(self):
1841 """Returns the most recent patchset number from the codereview site."""
1842 raise NotImplementedError()
1843
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001844 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1845 directory):
1846 """Fetches and applies the issue.
1847
1848 Arguments:
1849 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1850 reject: if True, reject the failed patch instead of switching to 3-way
1851 merge. Rietveld only.
1852 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1853 only.
1854 directory: switch to directory before applying the patch. Rietveld only.
1855 """
1856 raise NotImplementedError()
1857
1858 @staticmethod
1859 def ParseIssueURL(parsed_url):
1860 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1861 failed."""
1862 raise NotImplementedError()
1863
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 def EnsureAuthenticated(self, force):
1865 """Best effort check that user is authenticated with codereview server.
1866
1867 Arguments:
1868 force: whether to skip confirmation questions.
1869 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001870 raise NotImplementedError()
1871
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001872 def CMDUploadChange(self, options, args, change):
1873 """Uploads a change to codereview."""
1874 raise NotImplementedError()
1875
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001876 def SetCQState(self, new_state):
1877 """Update the CQ state for latest patchset.
1878
1879 Issue must have been already uploaded and known.
1880 """
1881 raise NotImplementedError()
1882
tandriie113dfd2016-10-11 10:20:12 -07001883 def CannotTriggerTryJobReason(self):
1884 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1885 raise NotImplementedError()
1886
tandriide281ae2016-10-12 06:02:30 -07001887 def GetIssueOwner(self):
1888 raise NotImplementedError()
1889
1890 def GetIssueProject(self):
1891 raise NotImplementedError()
1892
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001893
1894class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1895 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1896 super(_RietveldChangelistImpl, self).__init__(changelist)
1897 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001898 if not rietveld_server:
1899 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001900
1901 self._rietveld_server = rietveld_server
1902 self._auth_config = auth_config
1903 self._props = None
1904 self._rpc_server = None
1905
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906 def GetCodereviewServer(self):
1907 if not self._rietveld_server:
1908 # If we're on a branch then get the server potentially associated
1909 # with that branch.
1910 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001911 self._rietveld_server = gclient_utils.UpgradeToHttps(
1912 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001913 if not self._rietveld_server:
1914 self._rietveld_server = settings.GetDefaultServerUrl()
1915 return self._rietveld_server
1916
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001917 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001918 """Best effort check that user is authenticated with Rietveld server."""
1919 if self._auth_config.use_oauth2:
1920 authenticator = auth.get_authenticator_for_host(
1921 self.GetCodereviewServer(), self._auth_config)
1922 if not authenticator.has_cached_credentials():
1923 raise auth.LoginRequiredError(self.GetCodereviewServer())
1924
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 def FetchDescription(self):
1926 issue = self.GetIssue()
1927 assert issue
1928 try:
1929 return self.RpcServer().get_description(issue).strip()
1930 except urllib2.HTTPError as e:
1931 if e.code == 404:
1932 DieWithError(
1933 ('\nWhile fetching the description for issue %d, received a '
1934 '404 (not found)\n'
1935 'error. It is likely that you deleted this '
1936 'issue on the server. If this is the\n'
1937 'case, please run\n\n'
1938 ' git cl issue 0\n\n'
1939 'to clear the association with the deleted issue. Then run '
1940 'this command again.') % issue)
1941 else:
1942 DieWithError(
1943 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1944 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001945 print('Warning: Failed to retrieve CL description due to network '
1946 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 return ''
1948
1949 def GetMostRecentPatchset(self):
1950 return self.GetIssueProperties()['patchsets'][-1]
1951
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952 def GetIssueProperties(self):
1953 if self._props is None:
1954 issue = self.GetIssue()
1955 if not issue:
1956 self._props = {}
1957 else:
1958 self._props = self.RpcServer().get_issue_properties(issue, True)
1959 return self._props
1960
tandriie113dfd2016-10-11 10:20:12 -07001961 def CannotTriggerTryJobReason(self):
1962 props = self.GetIssueProperties()
1963 if not props:
1964 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1965 if props.get('closed'):
1966 return 'CL %s is closed' % self.GetIssue()
1967 if props.get('private'):
1968 return 'CL %s is private' % self.GetIssue()
1969 return None
1970
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001971 def GetApprovingReviewers(self):
1972 return get_approving_reviewers(self.GetIssueProperties())
1973
tandriide281ae2016-10-12 06:02:30 -07001974 def GetIssueOwner(self):
1975 return (self.GetIssueProperties() or {}).get('owner_email')
1976
1977 def GetIssueProject(self):
1978 return (self.GetIssueProperties() or {}).get('project')
1979
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001980 def AddComment(self, message):
1981 return self.RpcServer().add_comment(self.GetIssue(), message)
1982
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001983 def GetStatus(self):
1984 """Apply a rough heuristic to give a simple summary of an issue's review
1985 or CQ status, assuming adherence to a common workflow.
1986
1987 Returns None if no issue for this branch, or one of the following keywords:
1988 * 'error' - error from review tool (including deleted issues)
1989 * 'unsent' - not sent for review
1990 * 'waiting' - waiting for review
1991 * 'reply' - waiting for owner to reply to review
1992 * 'lgtm' - LGTM from at least one approved reviewer
1993 * 'commit' - in the commit queue
1994 * 'closed' - closed
1995 """
1996 if not self.GetIssue():
1997 return None
1998
1999 try:
2000 props = self.GetIssueProperties()
2001 except urllib2.HTTPError:
2002 return 'error'
2003
2004 if props.get('closed'):
2005 # Issue is closed.
2006 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002007 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002008 # Issue is in the commit queue.
2009 return 'commit'
2010
2011 try:
2012 reviewers = self.GetApprovingReviewers()
2013 except urllib2.HTTPError:
2014 return 'error'
2015
2016 if reviewers:
2017 # Was LGTM'ed.
2018 return 'lgtm'
2019
2020 messages = props.get('messages') or []
2021
tandrii9d2c7a32016-06-22 03:42:45 -07002022 # Skip CQ messages that don't require owner's action.
2023 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2024 if 'Dry run:' in messages[-1]['text']:
2025 messages.pop()
2026 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2027 # This message always follows prior messages from CQ,
2028 # so skip this too.
2029 messages.pop()
2030 else:
2031 # This is probably a CQ messages warranting user attention.
2032 break
2033
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002034 if not messages:
2035 # No message was sent.
2036 return 'unsent'
2037 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002038 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002039 return 'reply'
2040 return 'waiting'
2041
dsansomee2d6fd92016-09-08 00:10:47 -07002042 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002043 return self.RpcServer().update_description(
2044 self.GetIssue(), self.description)
2045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002047 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002049 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002050 return self.SetFlags({flag: value})
2051
2052 def SetFlags(self, flags):
2053 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002054 """
phajdan.jr68598232016-08-10 03:28:28 -07002055 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002056 try:
tandrii4b233bd2016-07-06 03:50:29 -07002057 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002058 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002059 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002060 if e.code == 404:
2061 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2062 if e.code == 403:
2063 DieWithError(
2064 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002065 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002066 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002068 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069 """Returns an upload.RpcServer() to access this review's rietveld instance.
2070 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002071 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002072 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002073 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002074 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002075 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002077 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002078 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002079 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002080
tandrii5d48c322016-08-18 16:19:37 -07002081 @classmethod
2082 def PatchsetConfigKey(cls):
2083 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084
tandrii5d48c322016-08-18 16:19:37 -07002085 @classmethod
2086 def CodereviewServerConfigKey(cls):
2087 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002088
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089 def GetRieveldObjForPresubmit(self):
2090 return self.RpcServer()
2091
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002092 def SetCQState(self, new_state):
2093 props = self.GetIssueProperties()
2094 if props.get('private'):
2095 DieWithError('Cannot set-commit on private issue')
2096
2097 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002098 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002099 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002100 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002101 else:
tandrii4b233bd2016-07-06 03:50:29 -07002102 assert new_state == _CQState.DRY_RUN
2103 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002104
2105
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002106 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2107 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002108 # PatchIssue should never be called with a dirty tree. It is up to the
2109 # caller to check this, but just in case we assert here since the
2110 # consequences of the caller not checking this could be dire.
2111 assert(not git_common.is_dirty_git_tree('apply'))
2112 assert(parsed_issue_arg.valid)
2113 self._changelist.issue = parsed_issue_arg.issue
2114 if parsed_issue_arg.hostname:
2115 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2116
skobes6468b902016-10-24 08:45:10 -07002117 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2118 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2119 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002120 try:
skobes6468b902016-10-24 08:45:10 -07002121 scm_obj.apply_patch(patchset_object)
2122 except Exception as e:
2123 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002124 return 1
2125
2126 # If we had an issue, commit the current state and register the issue.
2127 if not nocommit:
2128 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2129 'patch from issue %(i)s at patchset '
2130 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2131 % {'i': self.GetIssue(), 'p': patchset})])
2132 self.SetIssue(self.GetIssue())
2133 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002134 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002135 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002136 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002137 return 0
2138
2139 @staticmethod
2140 def ParseIssueURL(parsed_url):
2141 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2142 return None
wychen3c1c1722016-08-04 11:46:36 -07002143 # Rietveld patch: https://domain/<number>/#ps<patchset>
2144 match = re.match(r'/(\d+)/$', parsed_url.path)
2145 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2146 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002147 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002148 issue=int(match.group(1)),
2149 patchset=int(match2.group(1)),
2150 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 # Typical url: https://domain/<issue_number>[/[other]]
2152 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2153 if match:
skobes6468b902016-10-24 08:45:10 -07002154 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002155 issue=int(match.group(1)),
2156 hostname=parsed_url.netloc)
2157 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2158 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2159 if match:
skobes6468b902016-10-24 08:45:10 -07002160 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 issue=int(match.group(1)),
2162 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002163 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002164 return None
2165
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 def CMDUploadChange(self, options, args, change):
2167 """Upload the patch to Rietveld."""
2168 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2169 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002170 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2171 if options.emulate_svn_auto_props:
2172 upload_args.append('--emulate_svn_auto_props')
2173
2174 change_desc = None
2175
2176 if options.email is not None:
2177 upload_args.extend(['--email', options.email])
2178
2179 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002180 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002181 upload_args.extend(['--title', options.title])
2182 if options.message:
2183 upload_args.extend(['--message', options.message])
2184 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002185 print('This branch is associated with issue %s. '
2186 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002187 else:
nodirca166002016-06-27 10:59:51 -07002188 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189 upload_args.extend(['--title', options.title])
2190 message = (options.title or options.message or
2191 CreateDescriptionFromLog(args))
2192 change_desc = ChangeDescription(message)
2193 if options.reviewers or options.tbr_owners:
2194 change_desc.update_reviewers(options.reviewers,
2195 options.tbr_owners,
2196 change)
2197 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002198 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199
2200 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002201 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 return 1
2203
2204 upload_args.extend(['--message', change_desc.description])
2205 if change_desc.get_reviewers():
2206 upload_args.append('--reviewers=%s' % ','.join(
2207 change_desc.get_reviewers()))
2208 if options.send_mail:
2209 if not change_desc.get_reviewers():
2210 DieWithError("Must specify reviewers to send email.")
2211 upload_args.append('--send_mail')
2212
2213 # We check this before applying rietveld.private assuming that in
2214 # rietveld.cc only addresses which we can send private CLs to are listed
2215 # if rietveld.private is set, and so we should ignore rietveld.cc only
2216 # when --private is specified explicitly on the command line.
2217 if options.private:
2218 logging.warn('rietveld.cc is ignored since private flag is specified. '
2219 'You need to review and add them manually if necessary.')
2220 cc = self.GetCCListWithoutDefault()
2221 else:
2222 cc = self.GetCCList()
2223 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002224 if change_desc.get_cced():
2225 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226 if cc:
2227 upload_args.extend(['--cc', cc])
2228
2229 if options.private or settings.GetDefaultPrivateFlag() == "True":
2230 upload_args.append('--private')
2231
2232 upload_args.extend(['--git_similarity', str(options.similarity)])
2233 if not options.find_copies:
2234 upload_args.extend(['--git_no_find_copies'])
2235
2236 # Include the upstream repo's URL in the change -- this is useful for
2237 # projects that have their source spread across multiple repos.
2238 remote_url = self.GetGitBaseUrlFromConfig()
2239 if not remote_url:
2240 if settings.GetIsGitSvn():
2241 remote_url = self.GetGitSvnRemoteUrl()
2242 else:
2243 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2244 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2245 self.GetUpstreamBranch().split('/')[-1])
2246 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002247 remote, remote_branch = self.GetRemoteBranch()
2248 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2249 settings.GetPendingRefPrefix())
2250 if target_ref:
2251 upload_args.extend(['--target_ref', target_ref])
2252
2253 # Look for dependent patchsets. See crbug.com/480453 for more details.
2254 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2255 upstream_branch = ShortBranchName(upstream_branch)
2256 if remote is '.':
2257 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002258 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002259 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002260 print()
2261 print('Skipping dependency patchset upload because git config '
2262 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2263 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002264 else:
2265 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002266 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002267 auth_config=auth_config)
2268 branch_cl_issue_url = branch_cl.GetIssueURL()
2269 branch_cl_issue = branch_cl.GetIssue()
2270 branch_cl_patchset = branch_cl.GetPatchset()
2271 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2272 upload_args.extend(
2273 ['--depends_on_patchset', '%s:%s' % (
2274 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002275 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002276 '\n'
2277 'The current branch (%s) is tracking a local branch (%s) with '
2278 'an associated CL.\n'
2279 'Adding %s/#ps%s as a dependency patchset.\n'
2280 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2281 branch_cl_patchset))
2282
2283 project = settings.GetProject()
2284 if project:
2285 upload_args.extend(['--project', project])
2286
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287 try:
2288 upload_args = ['upload'] + upload_args + args
2289 logging.info('upload.RealMain(%s)', upload_args)
2290 issue, patchset = upload.RealMain(upload_args)
2291 issue = int(issue)
2292 patchset = int(patchset)
2293 except KeyboardInterrupt:
2294 sys.exit(1)
2295 except:
2296 # If we got an exception after the user typed a description for their
2297 # change, back up the description before re-raising.
2298 if change_desc:
2299 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2300 print('\nGot exception while uploading -- saving description to %s\n' %
2301 backup_path)
2302 backup_file = open(backup_path, 'w')
2303 backup_file.write(change_desc.description)
2304 backup_file.close()
2305 raise
2306
2307 if not self.GetIssue():
2308 self.SetIssue(issue)
2309 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002310 return 0
2311
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002312
2313class _GerritChangelistImpl(_ChangelistCodereviewBase):
2314 def __init__(self, changelist, auth_config=None):
2315 # auth_config is Rietveld thing, kept here to preserve interface only.
2316 super(_GerritChangelistImpl, self).__init__(changelist)
2317 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002318 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002319 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002320 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321
2322 def _GetGerritHost(self):
2323 # Lazy load of configs.
2324 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002325 if self._gerrit_host and '.' not in self._gerrit_host:
2326 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2327 # This happens for internal stuff http://crbug.com/614312.
2328 parsed = urlparse.urlparse(self.GetRemoteUrl())
2329 if parsed.scheme == 'sso':
2330 print('WARNING: using non https URLs for remote is likely broken\n'
2331 ' Your current remote is: %s' % self.GetRemoteUrl())
2332 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2333 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002334 return self._gerrit_host
2335
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 def _GetGitHost(self):
2337 """Returns git host to be used when uploading change to Gerrit."""
2338 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2339
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002340 def GetCodereviewServer(self):
2341 if not self._gerrit_server:
2342 # If we're on a branch then get the server potentially associated
2343 # with that branch.
2344 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002345 self._gerrit_server = self._GitGetBranchConfigValue(
2346 self.CodereviewServerConfigKey())
2347 if self._gerrit_server:
2348 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002349 if not self._gerrit_server:
2350 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2351 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002352 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002353 parts[0] = parts[0] + '-review'
2354 self._gerrit_host = '.'.join(parts)
2355 self._gerrit_server = 'https://%s' % self._gerrit_host
2356 return self._gerrit_server
2357
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002358 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002359 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002360 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002361
tandrii5d48c322016-08-18 16:19:37 -07002362 @classmethod
2363 def PatchsetConfigKey(cls):
2364 return 'gerritpatchset'
2365
2366 @classmethod
2367 def CodereviewServerConfigKey(cls):
2368 return 'gerritserver'
2369
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002370 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002371 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002372 if settings.GetGerritSkipEnsureAuthenticated():
2373 # For projects with unusual authentication schemes.
2374 # See http://crbug.com/603378.
2375 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002376 # Lazy-loader to identify Gerrit and Git hosts.
2377 if gerrit_util.GceAuthenticator.is_gce():
2378 return
2379 self.GetCodereviewServer()
2380 git_host = self._GetGitHost()
2381 assert self._gerrit_server and self._gerrit_host
2382 cookie_auth = gerrit_util.CookiesAuthenticator()
2383
2384 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2385 git_auth = cookie_auth.get_auth_header(git_host)
2386 if gerrit_auth and git_auth:
2387 if gerrit_auth == git_auth:
2388 return
2389 print((
2390 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2391 ' Check your %s or %s file for credentials of hosts:\n'
2392 ' %s\n'
2393 ' %s\n'
2394 ' %s') %
2395 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2396 git_host, self._gerrit_host,
2397 cookie_auth.get_new_password_message(git_host)))
2398 if not force:
2399 ask_for_data('If you know what you are doing, press Enter to continue, '
2400 'Ctrl+C to abort.')
2401 return
2402 else:
2403 missing = (
2404 [] if gerrit_auth else [self._gerrit_host] +
2405 [] if git_auth else [git_host])
2406 DieWithError('Credentials for the following hosts are required:\n'
2407 ' %s\n'
2408 'These are read from %s (or legacy %s)\n'
2409 '%s' % (
2410 '\n '.join(missing),
2411 cookie_auth.get_gitcookies_path(),
2412 cookie_auth.get_netrc_path(),
2413 cookie_auth.get_new_password_message(git_host)))
2414
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002415 def _PostUnsetIssueProperties(self):
2416 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002417 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002418
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002419 def GetRieveldObjForPresubmit(self):
2420 class ThisIsNotRietveldIssue(object):
2421 def __nonzero__(self):
2422 # This is a hack to make presubmit_support think that rietveld is not
2423 # defined, yet still ensure that calls directly result in a decent
2424 # exception message below.
2425 return False
2426
2427 def __getattr__(self, attr):
2428 print(
2429 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2430 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2431 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2432 'or use Rietveld for codereview.\n'
2433 'See also http://crbug.com/579160.' % attr)
2434 raise NotImplementedError()
2435 return ThisIsNotRietveldIssue()
2436
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002437 def GetGerritObjForPresubmit(self):
2438 return presubmit_support.GerritAccessor(self._GetGerritHost())
2439
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002440 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002441 """Apply a rough heuristic to give a simple summary of an issue's review
2442 or CQ status, assuming adherence to a common workflow.
2443
2444 Returns None if no issue for this branch, or one of the following keywords:
2445 * 'error' - error from review tool (including deleted issues)
2446 * 'unsent' - no reviewers added
2447 * 'waiting' - waiting for review
2448 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002449 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2450 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002451 * 'commit' - in the commit queue
2452 * 'closed' - abandoned
2453 """
2454 if not self.GetIssue():
2455 return None
2456
2457 try:
2458 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002459 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002460 return 'error'
2461
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002462 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002463 return 'closed'
2464
2465 cq_label = data['labels'].get('Commit-Queue', {})
2466 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002467 votes = cq_label.get('all', [])
2468 highest_vote = 0
2469 for v in votes:
2470 highest_vote = max(highest_vote, v.get('value', 0))
2471 vote_value = str(highest_vote)
2472 if vote_value != '0':
2473 # Add a '+' if the value is not 0 to match the values in the label.
2474 # The cq_label does not have negatives.
2475 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002476 vote_text = cq_label.get('values', {}).get(vote_value, '')
2477 if vote_text.lower() == 'commit':
2478 return 'commit'
2479
2480 lgtm_label = data['labels'].get('Code-Review', {})
2481 if lgtm_label:
2482 if 'rejected' in lgtm_label:
2483 return 'not lgtm'
2484 if 'approved' in lgtm_label:
2485 return 'lgtm'
2486
2487 if not data.get('reviewers', {}).get('REVIEWER', []):
2488 return 'unsent'
2489
2490 messages = data.get('messages', [])
2491 if messages:
2492 owner = data['owner'].get('_account_id')
2493 last_message_author = messages[-1].get('author', {}).get('_account_id')
2494 if owner != last_message_author:
2495 # Some reply from non-owner.
2496 return 'reply'
2497
2498 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002499
2500 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002501 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002502 return data['revisions'][data['current_revision']]['_number']
2503
2504 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002505 data = self._GetChangeDetail(['CURRENT_REVISION'])
2506 current_rev = data['current_revision']
2507 url = data['revisions'][current_rev]['fetch']['http']['url']
2508 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002509
dsansomee2d6fd92016-09-08 00:10:47 -07002510 def UpdateDescriptionRemote(self, description, force=False):
2511 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2512 if not force:
2513 ask_for_data(
2514 'The description cannot be modified while the issue has a pending '
2515 'unpublished edit. Either publish the edit in the Gerrit web UI '
2516 'or delete it.\n\n'
2517 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2518
2519 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2520 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002521 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2522 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002523
2524 def CloseIssue(self):
2525 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2526
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002527 def GetApprovingReviewers(self):
2528 """Returns a list of reviewers approving the change.
2529
2530 Note: not necessarily committers.
2531 """
2532 raise NotImplementedError()
2533
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002534 def SubmitIssue(self, wait_for_merge=True):
2535 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2536 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002538 def _GetChangeDetail(self, options=None, issue=None):
2539 options = options or []
2540 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002541 assert issue, 'issue is required to query Gerrit'
2542 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002543 options)
tandriic2405f52016-10-10 08:13:15 -07002544 if not data:
2545 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2546 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002547
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002548 def CMDLand(self, force, bypass_hooks, verbose):
2549 if git_common.is_dirty_git_tree('land'):
2550 return 1
tandriid60367b2016-06-22 05:25:12 -07002551 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2552 if u'Commit-Queue' in detail.get('labels', {}):
2553 if not force:
2554 ask_for_data('\nIt seems this repository has a Commit Queue, '
2555 'which can test and land changes for you. '
2556 'Are you sure you wish to bypass it?\n'
2557 'Press Enter to continue, Ctrl+C to abort.')
2558
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002559 differs = True
tandriic4344b52016-08-29 06:04:54 -07002560 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002561 # Note: git diff outputs nothing if there is no diff.
2562 if not last_upload or RunGit(['diff', last_upload]).strip():
2563 print('WARNING: some changes from local branch haven\'t been uploaded')
2564 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002565 if detail['current_revision'] == last_upload:
2566 differs = False
2567 else:
2568 print('WARNING: local branch contents differ from latest uploaded '
2569 'patchset')
2570 if differs:
2571 if not force:
2572 ask_for_data(
2573 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2574 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2575 elif not bypass_hooks:
2576 hook_results = self.RunHook(
2577 committing=True,
2578 may_prompt=not force,
2579 verbose=verbose,
2580 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2581 if not hook_results.should_continue():
2582 return 1
2583
2584 self.SubmitIssue(wait_for_merge=True)
2585 print('Issue %s has been submitted.' % self.GetIssueURL())
2586 return 0
2587
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002588 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2589 directory):
2590 assert not reject
2591 assert not nocommit
2592 assert not directory
2593 assert parsed_issue_arg.valid
2594
2595 self._changelist.issue = parsed_issue_arg.issue
2596
2597 if parsed_issue_arg.hostname:
2598 self._gerrit_host = parsed_issue_arg.hostname
2599 self._gerrit_server = 'https://%s' % self._gerrit_host
2600
tandriic2405f52016-10-10 08:13:15 -07002601 try:
2602 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2603 except GerritIssueNotExists as e:
2604 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002605
2606 if not parsed_issue_arg.patchset:
2607 # Use current revision by default.
2608 revision_info = detail['revisions'][detail['current_revision']]
2609 patchset = int(revision_info['_number'])
2610 else:
2611 patchset = parsed_issue_arg.patchset
2612 for revision_info in detail['revisions'].itervalues():
2613 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2614 break
2615 else:
2616 DieWithError('Couldn\'t find patchset %i in issue %i' %
2617 (parsed_issue_arg.patchset, self.GetIssue()))
2618
2619 fetch_info = revision_info['fetch']['http']
2620 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2621 RunGit(['cherry-pick', 'FETCH_HEAD'])
2622 self.SetIssue(self.GetIssue())
2623 self.SetPatchset(patchset)
2624 print('Committed patch for issue %i pathset %i locally' %
2625 (self.GetIssue(), self.GetPatchset()))
2626 return 0
2627
2628 @staticmethod
2629 def ParseIssueURL(parsed_url):
2630 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2631 return None
2632 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2633 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2634 # Short urls like https://domain/<issue_number> can be used, but don't allow
2635 # specifying the patchset (you'd 404), but we allow that here.
2636 if parsed_url.path == '/':
2637 part = parsed_url.fragment
2638 else:
2639 part = parsed_url.path
2640 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2641 if match:
2642 return _ParsedIssueNumberArgument(
2643 issue=int(match.group(2)),
2644 patchset=int(match.group(4)) if match.group(4) else None,
2645 hostname=parsed_url.netloc)
2646 return None
2647
tandrii16e0b4e2016-06-07 10:34:28 -07002648 def _GerritCommitMsgHookCheck(self, offer_removal):
2649 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2650 if not os.path.exists(hook):
2651 return
2652 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2653 # custom developer made one.
2654 data = gclient_utils.FileRead(hook)
2655 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2656 return
2657 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002658 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002659 'and may interfere with it in subtle ways.\n'
2660 'We recommend you remove the commit-msg hook.')
2661 if offer_removal:
2662 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2663 if reply.lower().startswith('y'):
2664 gclient_utils.rm_file_or_tree(hook)
2665 print('Gerrit commit-msg hook removed.')
2666 else:
2667 print('OK, will keep Gerrit commit-msg hook in place.')
2668
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002669 def CMDUploadChange(self, options, args, change):
2670 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002671 if options.squash and options.no_squash:
2672 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002673
2674 if not options.squash and not options.no_squash:
2675 # Load default for user, repo, squash=true, in this order.
2676 options.squash = settings.GetSquashGerritUploads()
2677 elif options.no_squash:
2678 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002679
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002680 # We assume the remote called "origin" is the one we want.
2681 # It is probably not worthwhile to support different workflows.
2682 gerrit_remote = 'origin'
2683
2684 remote, remote_branch = self.GetRemoteBranch()
2685 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2686 pending_prefix='')
2687
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002688 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002689 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002690 if self.GetIssue():
2691 # Try to get the message from a previous upload.
2692 message = self.GetDescription()
2693 if not message:
2694 DieWithError(
2695 'failed to fetch description from current Gerrit issue %d\n'
2696 '%s' % (self.GetIssue(), self.GetIssueURL()))
2697 change_id = self._GetChangeDetail()['change_id']
2698 while True:
2699 footer_change_ids = git_footers.get_footer_change_id(message)
2700 if footer_change_ids == [change_id]:
2701 break
2702 if not footer_change_ids:
2703 message = git_footers.add_footer_change_id(message, change_id)
2704 print('WARNING: appended missing Change-Id to issue description')
2705 continue
2706 # There is already a valid footer but with different or several ids.
2707 # Doing this automatically is non-trivial as we don't want to lose
2708 # existing other footers, yet we want to append just 1 desired
2709 # Change-Id. Thus, just create a new footer, but let user verify the
2710 # new description.
2711 message = '%s\n\nChange-Id: %s' % (message, change_id)
2712 print(
2713 'WARNING: issue %s has Change-Id footer(s):\n'
2714 ' %s\n'
2715 'but issue has Change-Id %s, according to Gerrit.\n'
2716 'Please, check the proposed correction to the description, '
2717 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2718 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2719 change_id))
2720 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2721 if not options.force:
2722 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002723 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002724 message = change_desc.description
2725 if not message:
2726 DieWithError("Description is empty. Aborting...")
2727 # Continue the while loop.
2728 # Sanity check of this code - we should end up with proper message
2729 # footer.
2730 assert [change_id] == git_footers.get_footer_change_id(message)
2731 change_desc = ChangeDescription(message)
2732 else:
2733 change_desc = ChangeDescription(
2734 options.message or CreateDescriptionFromLog(args))
2735 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002736 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 if not change_desc.description:
2738 DieWithError("Description is empty. Aborting...")
2739 message = change_desc.description
2740 change_ids = git_footers.get_footer_change_id(message)
2741 if len(change_ids) > 1:
2742 DieWithError('too many Change-Id footers, at most 1 allowed.')
2743 if not change_ids:
2744 # Generate the Change-Id automatically.
2745 message = git_footers.add_footer_change_id(
2746 message, GenerateGerritChangeId(message))
2747 change_desc.set_description(message)
2748 change_ids = git_footers.get_footer_change_id(message)
2749 assert len(change_ids) == 1
2750 change_id = change_ids[0]
2751
2752 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2753 if remote is '.':
2754 # If our upstream branch is local, we base our squashed commit on its
2755 # squashed version.
2756 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2757 # Check the squashed hash of the parent.
2758 parent = RunGit(['config',
2759 'branch.%s.gerritsquashhash' % upstream_branch_name],
2760 error_ok=True).strip()
2761 # Verify that the upstream branch has been uploaded too, otherwise
2762 # Gerrit will create additional CLs when uploading.
2763 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2764 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002765 DieWithError(
2766 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002767 'Note: maybe you\'ve uploaded it with --no-squash. '
2768 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769 ' git cl upload --squash\n' % upstream_branch_name)
2770 else:
2771 parent = self.GetCommonAncestorWithUpstream()
2772
2773 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2774 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2775 '-m', message]).strip()
2776 else:
2777 change_desc = ChangeDescription(
2778 options.message or CreateDescriptionFromLog(args))
2779 if not change_desc.description:
2780 DieWithError("Description is empty. Aborting...")
2781
2782 if not git_footers.get_footer_change_id(change_desc.description):
2783 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002784 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2785 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002786 ref_to_push = 'HEAD'
2787 parent = '%s/%s' % (gerrit_remote, branch)
2788 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2789
2790 assert change_desc
2791 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2792 ref_to_push)]).splitlines()
2793 if len(commits) > 1:
2794 print('WARNING: This will upload %d commits. Run the following command '
2795 'to see which commits will be uploaded: ' % len(commits))
2796 print('git log %s..%s' % (parent, ref_to_push))
2797 print('You can also use `git squash-branch` to squash these into a '
2798 'single commit.')
2799 ask_for_data('About to upload; enter to confirm.')
2800
2801 if options.reviewers or options.tbr_owners:
2802 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2803 change)
2804
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002805 # Extra options that can be specified at push time. Doc:
2806 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2807 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002808 if change_desc.get_reviewers(tbr_only=True):
2809 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2810 refspec_opts.append('l=Code-Review+1')
2811
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002812 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002813 if not re.match(r'^[\w ]+$', options.title):
2814 options.title = re.sub(r'[^\w ]', '', options.title)
2815 print('WARNING: Patchset title may only contain alphanumeric chars '
2816 'and spaces. Cleaned up title:\n%s' % options.title)
2817 if not options.force:
2818 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002819 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2820 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002821 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2822
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002823 if options.send_mail:
2824 if not change_desc.get_reviewers():
2825 DieWithError('Must specify reviewers to send email.')
2826 refspec_opts.append('notify=ALL')
2827 else:
2828 refspec_opts.append('notify=NONE')
2829
tandrii99a72f22016-08-17 14:33:24 -07002830 reviewers = change_desc.get_reviewers()
2831 if reviewers:
2832 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002833
agablec6787972016-09-09 16:13:34 -07002834 if options.private:
2835 refspec_opts.append('draft')
2836
rmistry9eadede2016-09-19 11:22:43 -07002837 if options.topic:
2838 # Documentation on Gerrit topics is here:
2839 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2840 refspec_opts.append('topic=%s' % options.topic)
2841
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002842 refspec_suffix = ''
2843 if refspec_opts:
2844 refspec_suffix = '%' + ','.join(refspec_opts)
2845 assert ' ' not in refspec_suffix, (
2846 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002847 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002848
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002849 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002850 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002851 print_stdout=True,
2852 # Flush after every line: useful for seeing progress when running as
2853 # recipe.
2854 filter_fn=lambda _: sys.stdout.flush())
2855
2856 if options.squash:
2857 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2858 change_numbers = [m.group(1)
2859 for m in map(regex.match, push_stdout.splitlines())
2860 if m]
2861 if len(change_numbers) != 1:
2862 DieWithError(
2863 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2864 'Change-Id: %s') % (len(change_numbers), change_id))
2865 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002866 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002867
2868 # Add cc's from the CC_LIST and --cc flag (if any).
2869 cc = self.GetCCList().split(',')
2870 if options.cc:
2871 cc.extend(options.cc)
2872 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002873 if change_desc.get_cced():
2874 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002875 if cc:
2876 gerrit_util.AddReviewers(
2877 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2878
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002879 return 0
2880
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002881 def _AddChangeIdToCommitMessage(self, options, args):
2882 """Re-commits using the current message, assumes the commit hook is in
2883 place.
2884 """
2885 log_desc = options.message or CreateDescriptionFromLog(args)
2886 git_command = ['commit', '--amend', '-m', log_desc]
2887 RunGit(git_command)
2888 new_log_desc = CreateDescriptionFromLog(args)
2889 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002890 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002891 return new_log_desc
2892 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002893 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002894
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002895 def SetCQState(self, new_state):
2896 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002897 vote_map = {
2898 _CQState.NONE: 0,
2899 _CQState.DRY_RUN: 1,
2900 _CQState.COMMIT : 2,
2901 }
2902 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2903 labels={'Commit-Queue': vote_map[new_state]})
2904
tandriie113dfd2016-10-11 10:20:12 -07002905 def CannotTriggerTryJobReason(self):
2906 # TODO(tandrii): implement for Gerrit.
2907 raise NotImplementedError()
2908
tandriide281ae2016-10-12 06:02:30 -07002909 def GetIssueOwner(self):
2910 # TODO(tandrii): implement for Gerrit.
2911 raise NotImplementedError()
2912
2913 def GetIssueProject(self):
2914 # TODO(tandrii): implement for Gerrit.
2915 raise NotImplementedError()
2916
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002917
2918_CODEREVIEW_IMPLEMENTATIONS = {
2919 'rietveld': _RietveldChangelistImpl,
2920 'gerrit': _GerritChangelistImpl,
2921}
2922
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002923
iannuccie53c9352016-08-17 14:40:40 -07002924def _add_codereview_issue_select_options(parser, extra=""):
2925 _add_codereview_select_options(parser)
2926
2927 text = ('Operate on this issue number instead of the current branch\'s '
2928 'implicit issue.')
2929 if extra:
2930 text += ' '+extra
2931 parser.add_option('-i', '--issue', type=int, help=text)
2932
2933
2934def _process_codereview_issue_select_options(parser, options):
2935 _process_codereview_select_options(parser, options)
2936 if options.issue is not None and not options.forced_codereview:
2937 parser.error('--issue must be specified with either --rietveld or --gerrit')
2938
2939
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002940def _add_codereview_select_options(parser):
2941 """Appends --gerrit and --rietveld options to force specific codereview."""
2942 parser.codereview_group = optparse.OptionGroup(
2943 parser, 'EXPERIMENTAL! Codereview override options')
2944 parser.add_option_group(parser.codereview_group)
2945 parser.codereview_group.add_option(
2946 '--gerrit', action='store_true',
2947 help='Force the use of Gerrit for codereview')
2948 parser.codereview_group.add_option(
2949 '--rietveld', action='store_true',
2950 help='Force the use of Rietveld for codereview')
2951
2952
2953def _process_codereview_select_options(parser, options):
2954 if options.gerrit and options.rietveld:
2955 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2956 options.forced_codereview = None
2957 if options.gerrit:
2958 options.forced_codereview = 'gerrit'
2959 elif options.rietveld:
2960 options.forced_codereview = 'rietveld'
2961
2962
tandriif9aefb72016-07-01 09:06:51 -07002963def _get_bug_line_values(default_project, bugs):
2964 """Given default_project and comma separated list of bugs, yields bug line
2965 values.
2966
2967 Each bug can be either:
2968 * a number, which is combined with default_project
2969 * string, which is left as is.
2970
2971 This function may produce more than one line, because bugdroid expects one
2972 project per line.
2973
2974 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2975 ['v8:123', 'chromium:789']
2976 """
2977 default_bugs = []
2978 others = []
2979 for bug in bugs.split(','):
2980 bug = bug.strip()
2981 if bug:
2982 try:
2983 default_bugs.append(int(bug))
2984 except ValueError:
2985 others.append(bug)
2986
2987 if default_bugs:
2988 default_bugs = ','.join(map(str, default_bugs))
2989 if default_project:
2990 yield '%s:%s' % (default_project, default_bugs)
2991 else:
2992 yield default_bugs
2993 for other in sorted(others):
2994 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2995 yield other
2996
2997
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002998class ChangeDescription(object):
2999 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003000 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003001 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003002 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003003
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003004 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003005 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003006
agable@chromium.org42c20792013-09-12 17:34:49 +00003007 @property # www.logilab.org/ticket/89786
3008 def description(self): # pylint: disable=E0202
3009 return '\n'.join(self._description_lines)
3010
3011 def set_description(self, desc):
3012 if isinstance(desc, basestring):
3013 lines = desc.splitlines()
3014 else:
3015 lines = [line.rstrip() for line in desc]
3016 while lines and not lines[0]:
3017 lines.pop(0)
3018 while lines and not lines[-1]:
3019 lines.pop(-1)
3020 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021
piman@chromium.org336f9122014-09-04 02:16:55 +00003022 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003025 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003026 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003028
agable@chromium.org42c20792013-09-12 17:34:49 +00003029 # Get the set of R= and TBR= lines and remove them from the desciption.
3030 regexp = re.compile(self.R_LINE)
3031 matches = [regexp.match(line) for line in self._description_lines]
3032 new_desc = [l for i, l in enumerate(self._description_lines)
3033 if not matches[i]]
3034 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003035
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 # Construct new unified R= and TBR= lines.
3037 r_names = []
3038 tbr_names = []
3039 for match in matches:
3040 if not match:
3041 continue
3042 people = cleanup_list([match.group(2).strip()])
3043 if match.group(1) == 'TBR':
3044 tbr_names.extend(people)
3045 else:
3046 r_names.extend(people)
3047 for name in r_names:
3048 if name not in reviewers:
3049 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003050 if add_owners_tbr:
3051 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003052 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003053 all_reviewers = set(tbr_names + reviewers)
3054 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3055 all_reviewers)
3056 tbr_names.extend(owners_db.reviewers_for(missing_files,
3057 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003058 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3059 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3060
3061 # Put the new lines in the description where the old first R= line was.
3062 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3063 if 0 <= line_loc < len(self._description_lines):
3064 if new_tbr_line:
3065 self._description_lines.insert(line_loc, new_tbr_line)
3066 if new_r_line:
3067 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003068 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003069 if new_r_line:
3070 self.append_footer(new_r_line)
3071 if new_tbr_line:
3072 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073
tandriif9aefb72016-07-01 09:06:51 -07003074 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 self.set_description([
3077 '# Enter a description of the change.',
3078 '# This will be displayed on the codereview site.',
3079 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003080 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003081 '--------------------',
3082 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 regexp = re.compile(self.BUG_LINE)
3085 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003086 prefix = settings.GetBugPrefix()
3087 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3088 for value in values:
3089 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3090 self.append_footer('BUG=%s' % value)
3091
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003093 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003094 if not content:
3095 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003097
3098 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003099 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3100 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003101 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003103
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003104 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003105 """Adds a footer line to the description.
3106
3107 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3108 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3109 that Gerrit footers are always at the end.
3110 """
3111 parsed_footer_line = git_footers.parse_footer(line)
3112 if parsed_footer_line:
3113 # Line is a gerrit footer in the form: Footer-Key: any value.
3114 # Thus, must be appended observing Gerrit footer rules.
3115 self.set_description(
3116 git_footers.add_footer(self.description,
3117 key=parsed_footer_line[0],
3118 value=parsed_footer_line[1]))
3119 return
3120
3121 if not self._description_lines:
3122 self._description_lines.append(line)
3123 return
3124
3125 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3126 if gerrit_footers:
3127 # git_footers.split_footers ensures that there is an empty line before
3128 # actual (gerrit) footers, if any. We have to keep it that way.
3129 assert top_lines and top_lines[-1] == ''
3130 top_lines, separator = top_lines[:-1], top_lines[-1:]
3131 else:
3132 separator = [] # No need for separator if there are no gerrit_footers.
3133
3134 prev_line = top_lines[-1] if top_lines else ''
3135 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3136 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3137 top_lines.append('')
3138 top_lines.append(line)
3139 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003140
tandrii99a72f22016-08-17 14:33:24 -07003141 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003142 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003143 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003144 reviewers = [match.group(2).strip()
3145 for match in matches
3146 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003148
bradnelsond975b302016-10-23 12:20:23 -07003149 def get_cced(self):
3150 """Retrieves the list of reviewers."""
3151 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3152 cced = [match.group(2).strip() for match in matches if match]
3153 return cleanup_list(cced)
3154
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003155
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003156def get_approving_reviewers(props):
3157 """Retrieves the reviewers that approved a CL from the issue properties with
3158 messages.
3159
3160 Note that the list may contain reviewers that are not committer, thus are not
3161 considered by the CQ.
3162 """
3163 return sorted(
3164 set(
3165 message['sender']
3166 for message in props['messages']
3167 if message['approval'] and message['sender'] in props['reviewers']
3168 )
3169 )
3170
3171
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003172def FindCodereviewSettingsFile(filename='codereview.settings'):
3173 """Finds the given file starting in the cwd and going up.
3174
3175 Only looks up to the top of the repository unless an
3176 'inherit-review-settings-ok' file exists in the root of the repository.
3177 """
3178 inherit_ok_file = 'inherit-review-settings-ok'
3179 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003180 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3182 root = '/'
3183 while True:
3184 if filename in os.listdir(cwd):
3185 if os.path.isfile(os.path.join(cwd, filename)):
3186 return open(os.path.join(cwd, filename))
3187 if cwd == root:
3188 break
3189 cwd = os.path.dirname(cwd)
3190
3191
3192def LoadCodereviewSettingsFromFile(fileobj):
3193 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003194 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003195
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003196 def SetProperty(name, setting, unset_error_ok=False):
3197 fullname = 'rietveld.' + name
3198 if setting in keyvals:
3199 RunGit(['config', fullname, keyvals[setting]])
3200 else:
3201 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3202
tandrii48df5812016-10-17 03:55:37 -07003203 if not keyvals.get('GERRIT_HOST', False):
3204 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 # Only server setting is required. Other settings can be absent.
3206 # In that case, we ignore errors raised during option deletion attempt.
3207 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003208 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003209 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3210 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003211 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003212 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003213 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3214 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003215 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003216 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003217 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003218 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003219 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3220 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003222 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003223 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003224
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003225 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003226 RunGit(['config', 'gerrit.squash-uploads',
3227 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003228
tandrii@chromium.org28253532016-04-14 13:46:56 +00003229 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003230 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003231 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3234 #should be of the form
3235 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3236 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3237 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3238 keyvals['ORIGIN_URL_CONFIG']])
3239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003241def urlretrieve(source, destination):
3242 """urllib is broken for SSL connections via a proxy therefore we
3243 can't use urllib.urlretrieve()."""
3244 with open(destination, 'w') as f:
3245 f.write(urllib2.urlopen(source).read())
3246
3247
ukai@chromium.org712d6102013-11-27 00:52:58 +00003248def hasSheBang(fname):
3249 """Checks fname is a #! script."""
3250 with open(fname) as f:
3251 return f.read(2).startswith('#!')
3252
3253
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003254# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3255def DownloadHooks(*args, **kwargs):
3256 pass
3257
3258
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003259def DownloadGerritHook(force):
3260 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003261
3262 Args:
3263 force: True to update hooks. False to install hooks if not present.
3264 """
3265 if not settings.GetIsGerrit():
3266 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003267 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003268 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3269 if not os.access(dst, os.X_OK):
3270 if os.path.exists(dst):
3271 if not force:
3272 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003273 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003274 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003275 if not hasSheBang(dst):
3276 DieWithError('Not a script: %s\n'
3277 'You need to download from\n%s\n'
3278 'into .git/hooks/commit-msg and '
3279 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003280 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3281 except Exception:
3282 if os.path.exists(dst):
3283 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003284 DieWithError('\nFailed to download hooks.\n'
3285 'You need to download from\n%s\n'
3286 'into .git/hooks/commit-msg and '
3287 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003288
3289
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003290
3291def GetRietveldCodereviewSettingsInteractively():
3292 """Prompt the user for settings."""
3293 server = settings.GetDefaultServerUrl(error_ok=True)
3294 prompt = 'Rietveld server (host[:port])'
3295 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3296 newserver = ask_for_data(prompt + ':')
3297 if not server and not newserver:
3298 newserver = DEFAULT_SERVER
3299 if newserver:
3300 newserver = gclient_utils.UpgradeToHttps(newserver)
3301 if newserver != server:
3302 RunGit(['config', 'rietveld.server', newserver])
3303
3304 def SetProperty(initial, caption, name, is_url):
3305 prompt = caption
3306 if initial:
3307 prompt += ' ("x" to clear) [%s]' % initial
3308 new_val = ask_for_data(prompt + ':')
3309 if new_val == 'x':
3310 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3311 elif new_val:
3312 if is_url:
3313 new_val = gclient_utils.UpgradeToHttps(new_val)
3314 if new_val != initial:
3315 RunGit(['config', 'rietveld.' + name, new_val])
3316
3317 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3318 SetProperty(settings.GetDefaultPrivateFlag(),
3319 'Private flag (rietveld only)', 'private', False)
3320 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3321 'tree-status-url', False)
3322 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3323 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3324 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3325 'run-post-upload-hook', False)
3326
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003327@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003328def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003329 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330
tandrii5d0a0422016-09-14 06:24:35 -07003331 print('WARNING: git cl config works for Rietveld only')
3332 # TODO(tandrii): remove this once we switch to Gerrit.
3333 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003334 parser.add_option('--activate-update', action='store_true',
3335 help='activate auto-updating [rietveld] section in '
3336 '.git/config')
3337 parser.add_option('--deactivate-update', action='store_true',
3338 help='deactivate auto-updating [rietveld] section in '
3339 '.git/config')
3340 options, args = parser.parse_args(args)
3341
3342 if options.deactivate_update:
3343 RunGit(['config', 'rietveld.autoupdate', 'false'])
3344 return
3345
3346 if options.activate_update:
3347 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3348 return
3349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003350 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003351 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003352 return 0
3353
3354 url = args[0]
3355 if not url.endswith('codereview.settings'):
3356 url = os.path.join(url, 'codereview.settings')
3357
3358 # Load code review settings and download hooks (if available).
3359 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3360 return 0
3361
3362
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003363def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003364 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003365 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3366 branch = ShortBranchName(branchref)
3367 _, args = parser.parse_args(args)
3368 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003369 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003370 return RunGit(['config', 'branch.%s.base-url' % branch],
3371 error_ok=False).strip()
3372 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003373 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003374 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3375 error_ok=False).strip()
3376
3377
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003378def color_for_status(status):
3379 """Maps a Changelist status to color, for CMDstatus and other tools."""
3380 return {
3381 'unsent': Fore.RED,
3382 'waiting': Fore.BLUE,
3383 'reply': Fore.YELLOW,
3384 'lgtm': Fore.GREEN,
3385 'commit': Fore.MAGENTA,
3386 'closed': Fore.CYAN,
3387 'error': Fore.WHITE,
3388 }.get(status, Fore.WHITE)
3389
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003390
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003391def get_cl_statuses(changes, fine_grained, max_processes=None):
3392 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003393
3394 If fine_grained is true, this will fetch CL statuses from the server.
3395 Otherwise, simply indicate if there's a matching url for the given branches.
3396
3397 If max_processes is specified, it is used as the maximum number of processes
3398 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3399 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003400
3401 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003402 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003403 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003404 upload.verbosity = 0
3405
3406 if fine_grained:
3407 # Process one branch synchronously to work through authentication, then
3408 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003409 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003410 def fetch(cl):
3411 try:
3412 return (cl, cl.GetStatus())
3413 except:
3414 # See http://crbug.com/629863.
3415 logging.exception('failed to fetch status for %s:', cl)
3416 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003417 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003418
tandriiea9514a2016-08-17 12:32:37 -07003419 changes_to_fetch = changes[1:]
3420 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003421 # Exit early if there was only one branch to fetch.
3422 return
3423
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003424 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003425 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003426 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003427 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003428
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003429 fetched_cls = set()
3430 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003431 while True:
3432 try:
3433 row = it.next(timeout=5)
3434 except multiprocessing.TimeoutError:
3435 break
3436
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003437 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003438 yield row
3439
3440 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003441 for cl in set(changes_to_fetch) - fetched_cls:
3442 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003443
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003444 else:
3445 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003446 for cl in changes:
3447 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003448
rmistry@google.com2dd99862015-06-22 12:22:18 +00003449
3450def upload_branch_deps(cl, args):
3451 """Uploads CLs of local branches that are dependents of the current branch.
3452
3453 If the local branch dependency tree looks like:
3454 test1 -> test2.1 -> test3.1
3455 -> test3.2
3456 -> test2.2 -> test3.3
3457
3458 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3459 run on the dependent branches in this order:
3460 test2.1, test3.1, test3.2, test2.2, test3.3
3461
3462 Note: This function does not rebase your local dependent branches. Use it when
3463 you make a change to the parent branch that will not conflict with its
3464 dependent branches, and you would like their dependencies updated in
3465 Rietveld.
3466 """
3467 if git_common.is_dirty_git_tree('upload-branch-deps'):
3468 return 1
3469
3470 root_branch = cl.GetBranch()
3471 if root_branch is None:
3472 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3473 'Get on a branch!')
3474 if not cl.GetIssue() or not cl.GetPatchset():
3475 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3476 'patchset dependencies without an uploaded CL.')
3477
3478 branches = RunGit(['for-each-ref',
3479 '--format=%(refname:short) %(upstream:short)',
3480 'refs/heads'])
3481 if not branches:
3482 print('No local branches found.')
3483 return 0
3484
3485 # Create a dictionary of all local branches to the branches that are dependent
3486 # on it.
3487 tracked_to_dependents = collections.defaultdict(list)
3488 for b in branches.splitlines():
3489 tokens = b.split()
3490 if len(tokens) == 2:
3491 branch_name, tracked = tokens
3492 tracked_to_dependents[tracked].append(branch_name)
3493
vapiera7fbd5a2016-06-16 09:17:49 -07003494 print()
3495 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003496 dependents = []
3497 def traverse_dependents_preorder(branch, padding=''):
3498 dependents_to_process = tracked_to_dependents.get(branch, [])
3499 padding += ' '
3500 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003501 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003502 dependents.append(dependent)
3503 traverse_dependents_preorder(dependent, padding)
3504 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003505 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003506
3507 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003508 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003509 return 0
3510
vapiera7fbd5a2016-06-16 09:17:49 -07003511 print('This command will checkout all dependent branches and run '
3512 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003513 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3514
andybons@chromium.org962f9462016-02-03 20:00:42 +00003515 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003516 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003517 args.extend(['-t', 'Updated patchset dependency'])
3518
rmistry@google.com2dd99862015-06-22 12:22:18 +00003519 # Record all dependents that failed to upload.
3520 failures = {}
3521 # Go through all dependents, checkout the branch and upload.
3522 try:
3523 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003524 print()
3525 print('--------------------------------------')
3526 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003527 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003528 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003529 try:
3530 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003531 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003532 failures[dependent_branch] = 1
3533 except: # pylint: disable=W0702
3534 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003535 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003536 finally:
3537 # Swap back to the original root branch.
3538 RunGit(['checkout', '-q', root_branch])
3539
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print()
3541 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003542 for dependent_branch in dependents:
3543 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003544 print(' %s : %s' % (dependent_branch, upload_status))
3545 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003546
3547 return 0
3548
3549
kmarshall3bff56b2016-06-06 18:31:47 -07003550def CMDarchive(parser, args):
3551 """Archives and deletes branches associated with closed changelists."""
3552 parser.add_option(
3553 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003554 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003555 parser.add_option(
3556 '-f', '--force', action='store_true',
3557 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003558 parser.add_option(
3559 '-d', '--dry-run', action='store_true',
3560 help='Skip the branch tagging and removal steps.')
3561 parser.add_option(
3562 '-t', '--notags', action='store_true',
3563 help='Do not tag archived branches. '
3564 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003565
3566 auth.add_auth_options(parser)
3567 options, args = parser.parse_args(args)
3568 if args:
3569 parser.error('Unsupported args: %s' % ' '.join(args))
3570 auth_config = auth.extract_auth_config_from_options(options)
3571
3572 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3573 if not branches:
3574 return 0
3575
vapiera7fbd5a2016-06-16 09:17:49 -07003576 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003577 changes = [Changelist(branchref=b, auth_config=auth_config)
3578 for b in branches.splitlines()]
3579 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3580 statuses = get_cl_statuses(changes,
3581 fine_grained=True,
3582 max_processes=options.maxjobs)
3583 proposal = [(cl.GetBranch(),
3584 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3585 for cl, status in statuses
3586 if status == 'closed']
3587 proposal.sort()
3588
3589 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003591 return 0
3592
3593 current_branch = GetCurrentBranch()
3594
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003596 if options.notags:
3597 for next_item in proposal:
3598 print(' ' + next_item[0])
3599 else:
3600 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3601 for next_item in proposal:
3602 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003603
kmarshall9249e012016-08-23 12:02:16 -07003604 # Quit now on precondition failure or if instructed by the user, either
3605 # via an interactive prompt or by command line flags.
3606 if options.dry_run:
3607 print('\nNo changes were made (dry run).\n')
3608 return 0
3609 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003610 print('You are currently on a branch \'%s\' which is associated with a '
3611 'closed codereview issue, so archive cannot proceed. Please '
3612 'checkout another branch and run this command again.' %
3613 current_branch)
3614 return 1
kmarshall9249e012016-08-23 12:02:16 -07003615 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003616 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3617 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003618 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003619 return 1
3620
3621 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003622 if not options.notags:
3623 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003624 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003625
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003627
3628 return 0
3629
3630
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003631def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003632 """Show status of changelists.
3633
3634 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003635 - Red not sent for review or broken
3636 - Blue waiting for review
3637 - Yellow waiting for you to reply to review
3638 - Green LGTM'ed
3639 - Magenta in the commit queue
3640 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003641
3642 Also see 'git cl comments'.
3643 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003645 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003646 parser.add_option('-f', '--fast', action='store_true',
3647 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003648 parser.add_option(
3649 '-j', '--maxjobs', action='store', type=int,
3650 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003651
3652 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003653 _add_codereview_issue_select_options(
3654 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003655 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003656 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003657 if args:
3658 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003659 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660
iannuccie53c9352016-08-17 14:40:40 -07003661 if options.issue is not None and not options.field:
3662 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003665 cl = Changelist(auth_config=auth_config, issue=options.issue,
3666 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003667 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003668 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 elif options.field == 'id':
3670 issueid = cl.GetIssue()
3671 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003673 elif options.field == 'patch':
3674 patchset = cl.GetPatchset()
3675 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003676 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003677 elif options.field == 'status':
3678 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679 elif options.field == 'url':
3680 url = cl.GetIssueURL()
3681 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003682 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003683 return 0
3684
3685 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3686 if not branches:
3687 print('No local branch found.')
3688 return 0
3689
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003690 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003691 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003692 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003693 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003694 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003695 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003696 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003697
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003698 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003699 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3700 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3701 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003702 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003703 c, status = output.next()
3704 branch_statuses[c.GetBranch()] = status
3705 status = branch_statuses.pop(branch)
3706 url = cl.GetIssueURL()
3707 if url and (not status or status == 'error'):
3708 # The issue probably doesn't exist anymore.
3709 url += ' (broken)'
3710
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003711 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003712 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003713 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003714 color = ''
3715 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003716 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003718 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003719 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003720
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003721 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print()
3723 print('Current branch:',)
3724 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003725 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003727 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003729 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003730 print('Issue description:')
3731 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003732 return 0
3733
3734
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003735def colorize_CMDstatus_doc():
3736 """To be called once in main() to add colors to git cl status help."""
3737 colors = [i for i in dir(Fore) if i[0].isupper()]
3738
3739 def colorize_line(line):
3740 for color in colors:
3741 if color in line.upper():
3742 # Extract whitespaces first and the leading '-'.
3743 indent = len(line) - len(line.lstrip(' ')) + 1
3744 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3745 return line
3746
3747 lines = CMDstatus.__doc__.splitlines()
3748 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3749
3750
phajdan.jre328cf92016-08-22 04:12:17 -07003751def write_json(path, contents):
3752 with open(path, 'w') as f:
3753 json.dump(contents, f)
3754
3755
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003756@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003757def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003758 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003759
3760 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003761 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003762 parser.add_option('-r', '--reverse', action='store_true',
3763 help='Lookup the branch(es) for the specified issues. If '
3764 'no issues are specified, all branches with mapped '
3765 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003766 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003767 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003768 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003769 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003770
dnj@chromium.org406c4402015-03-03 17:22:28 +00003771 if options.reverse:
3772 branches = RunGit(['for-each-ref', 'refs/heads',
3773 '--format=%(refname:short)']).splitlines()
3774
3775 # Reverse issue lookup.
3776 issue_branch_map = {}
3777 for branch in branches:
3778 cl = Changelist(branchref=branch)
3779 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3780 if not args:
3781 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003782 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003783 for issue in args:
3784 if not issue:
3785 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003786 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003787 print('Branch for issue number %s: %s' % (
3788 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003789 if options.json:
3790 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003791 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003792 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003793 if len(args) > 0:
3794 try:
3795 issue = int(args[0])
3796 except ValueError:
3797 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003798 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003799 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003800 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003801 if options.json:
3802 write_json(options.json, {
3803 'issue': cl.GetIssue(),
3804 'issue_url': cl.GetIssueURL(),
3805 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 return 0
3807
3808
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003809def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003810 """Shows or posts review comments for any changelist."""
3811 parser.add_option('-a', '--add-comment', dest='comment',
3812 help='comment to add to an issue')
3813 parser.add_option('-i', dest='issue',
3814 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003815 parser.add_option('-j', '--json-file',
3816 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003817 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003818 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003819 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003820
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003821 issue = None
3822 if options.issue:
3823 try:
3824 issue = int(options.issue)
3825 except ValueError:
3826 DieWithError('A review issue id is expected to be a number')
3827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003828 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003829
3830 if options.comment:
3831 cl.AddComment(options.comment)
3832 return 0
3833
3834 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003835 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003836 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003837 summary.append({
3838 'date': message['date'],
3839 'lgtm': False,
3840 'message': message['text'],
3841 'not_lgtm': False,
3842 'sender': message['sender'],
3843 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003844 if message['disapproval']:
3845 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003846 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003847 elif message['approval']:
3848 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003849 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003850 elif message['sender'] == data['owner_email']:
3851 color = Fore.MAGENTA
3852 else:
3853 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003854 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003855 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003856 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003857 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003858 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003859 if options.json_file:
3860 with open(options.json_file, 'wb') as f:
3861 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003862 return 0
3863
3864
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003865@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003866def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003867 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003868 parser.add_option('-d', '--display', action='store_true',
3869 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003870 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003871 help='New description to set for this issue (- for stdin, '
3872 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003873 parser.add_option('-f', '--force', action='store_true',
3874 help='Delete any unpublished Gerrit edits for this issue '
3875 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003876
3877 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003878 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003879 options, args = parser.parse_args(args)
3880 _process_codereview_select_options(parser, options)
3881
3882 target_issue = None
3883 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003884 target_issue = ParseIssueNumberArgument(args[0])
3885 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003886 parser.print_help()
3887 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003888
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003889 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003890
martiniss6eda05f2016-06-30 10:18:35 -07003891 kwargs = {
3892 'auth_config': auth_config,
3893 'codereview': options.forced_codereview,
3894 }
3895 if target_issue:
3896 kwargs['issue'] = target_issue.issue
3897 if options.forced_codereview == 'rietveld':
3898 kwargs['rietveld_server'] = target_issue.hostname
3899
3900 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003901
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003902 if not cl.GetIssue():
3903 DieWithError('This branch has no associated changelist.')
3904 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003905
smut@google.com34fb6b12015-07-13 20:03:26 +00003906 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003907 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003908 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003909
3910 if options.new_description:
3911 text = options.new_description
3912 if text == '-':
3913 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003914 elif text == '+':
3915 base_branch = cl.GetCommonAncestorWithUpstream()
3916 change = cl.GetChange(base_branch, None, local_description=True)
3917 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003918
3919 description.set_description(text)
3920 else:
3921 description.prompt()
3922
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003923 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003924 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003925 return 0
3926
3927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928def CreateDescriptionFromLog(args):
3929 """Pulls out the commit log to use as a base for the CL description."""
3930 log_args = []
3931 if len(args) == 1 and not args[0].endswith('.'):
3932 log_args = [args[0] + '..']
3933 elif len(args) == 1 and args[0].endswith('...'):
3934 log_args = [args[0][:-1]]
3935 elif len(args) == 2:
3936 log_args = [args[0] + '..' + args[1]]
3937 else:
3938 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003939 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940
3941
thestig@chromium.org44202a22014-03-11 19:22:18 +00003942def CMDlint(parser, args):
3943 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003944 parser.add_option('--filter', action='append', metavar='-x,+y',
3945 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003946 auth.add_auth_options(parser)
3947 options, args = parser.parse_args(args)
3948 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003949
3950 # Access to a protected member _XX of a client class
3951 # pylint: disable=W0212
3952 try:
3953 import cpplint
3954 import cpplint_chromium
3955 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003956 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003957 return 1
3958
3959 # Change the current working directory before calling lint so that it
3960 # shows the correct base.
3961 previous_cwd = os.getcwd()
3962 os.chdir(settings.GetRoot())
3963 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003964 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003965 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3966 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003967 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003968 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003969 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003970
3971 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003972 command = args + files
3973 if options.filter:
3974 command = ['--filter=' + ','.join(options.filter)] + command
3975 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003976
3977 white_regex = re.compile(settings.GetLintRegex())
3978 black_regex = re.compile(settings.GetLintIgnoreRegex())
3979 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3980 for filename in filenames:
3981 if white_regex.match(filename):
3982 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003983 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003984 else:
3985 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3986 extra_check_functions)
3987 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003988 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003989 finally:
3990 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003991 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003992 if cpplint._cpplint_state.error_count != 0:
3993 return 1
3994 return 0
3995
3996
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003998 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003999 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004001 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004002 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004003 auth.add_auth_options(parser)
4004 options, args = parser.parse_args(args)
4005 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006
sbc@chromium.org71437c02015-04-09 19:29:40 +00004007 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004008 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004009 return 1
4010
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004011 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004012 if args:
4013 base_branch = args[0]
4014 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004015 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004016 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004017
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004018 cl.RunHook(
4019 committing=not options.upload,
4020 may_prompt=False,
4021 verbose=options.verbose,
4022 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004023 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004024
4025
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004026def GenerateGerritChangeId(message):
4027 """Returns Ixxxxxx...xxx change id.
4028
4029 Works the same way as
4030 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4031 but can be called on demand on all platforms.
4032
4033 The basic idea is to generate git hash of a state of the tree, original commit
4034 message, author/committer info and timestamps.
4035 """
4036 lines = []
4037 tree_hash = RunGitSilent(['write-tree'])
4038 lines.append('tree %s' % tree_hash.strip())
4039 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4040 if code == 0:
4041 lines.append('parent %s' % parent.strip())
4042 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4043 lines.append('author %s' % author.strip())
4044 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4045 lines.append('committer %s' % committer.strip())
4046 lines.append('')
4047 # Note: Gerrit's commit-hook actually cleans message of some lines and
4048 # whitespace. This code is not doing this, but it clearly won't decrease
4049 # entropy.
4050 lines.append(message)
4051 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4052 stdin='\n'.join(lines))
4053 return 'I%s' % change_hash.strip()
4054
4055
wittman@chromium.org455dc922015-01-26 20:15:50 +00004056def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4057 """Computes the remote branch ref to use for the CL.
4058
4059 Args:
4060 remote (str): The git remote for the CL.
4061 remote_branch (str): The git remote branch for the CL.
4062 target_branch (str): The target branch specified by the user.
4063 pending_prefix (str): The pending prefix from the settings.
4064 """
4065 if not (remote and remote_branch):
4066 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004067
wittman@chromium.org455dc922015-01-26 20:15:50 +00004068 if target_branch:
4069 # Cannonicalize branch references to the equivalent local full symbolic
4070 # refs, which are then translated into the remote full symbolic refs
4071 # below.
4072 if '/' not in target_branch:
4073 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4074 else:
4075 prefix_replacements = (
4076 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4077 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4078 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4079 )
4080 match = None
4081 for regex, replacement in prefix_replacements:
4082 match = re.search(regex, target_branch)
4083 if match:
4084 remote_branch = target_branch.replace(match.group(0), replacement)
4085 break
4086 if not match:
4087 # This is a branch path but not one we recognize; use as-is.
4088 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004089 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4090 # Handle the refs that need to land in different refs.
4091 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004092
wittman@chromium.org455dc922015-01-26 20:15:50 +00004093 # Create the true path to the remote branch.
4094 # Does the following translation:
4095 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4096 # * refs/remotes/origin/master -> refs/heads/master
4097 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4098 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4099 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4100 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4101 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4102 'refs/heads/')
4103 elif remote_branch.startswith('refs/remotes/branch-heads'):
4104 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4105 # If a pending prefix exists then replace refs/ with it.
4106 if pending_prefix:
4107 remote_branch = remote_branch.replace('refs/', pending_prefix)
4108 return remote_branch
4109
4110
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004111def cleanup_list(l):
4112 """Fixes a list so that comma separated items are put as individual items.
4113
4114 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4115 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4116 """
4117 items = sum((i.split(',') for i in l), [])
4118 stripped_items = (i.strip() for i in items)
4119 return sorted(filter(None, stripped_items))
4120
4121
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004122@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004123def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004124 """Uploads the current changelist to codereview.
4125
4126 Can skip dependency patchset uploads for a branch by running:
4127 git config branch.branch_name.skip-deps-uploads True
4128 To unset run:
4129 git config --unset branch.branch_name.skip-deps-uploads
4130 Can also set the above globally by using the --global flag.
4131 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004132 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4133 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004134 parser.add_option('--bypass-watchlists', action='store_true',
4135 dest='bypass_watchlists',
4136 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004137 parser.add_option('-f', action='store_true', dest='force',
4138 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004139 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004140 parser.add_option('-b', '--bug',
4141 help='pre-populate the bug number(s) for this issue. '
4142 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004143 parser.add_option('--message-file', dest='message_file',
4144 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004145 parser.add_option('-t', dest='title',
4146 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004147 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004148 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004149 help='reviewer email addresses')
4150 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004151 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004152 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004153 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004154 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004155 parser.add_option('--emulate_svn_auto_props',
4156 '--emulate-svn-auto-props',
4157 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004158 dest="emulate_svn_auto_props",
4159 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004160 parser.add_option('-c', '--use-commit-queue', action='store_true',
4161 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004162 parser.add_option('--private', action='store_true',
4163 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004164 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004165 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004166 metavar='TARGET',
4167 help='Apply CL to remote ref TARGET. ' +
4168 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004169 parser.add_option('--squash', action='store_true',
4170 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004171 parser.add_option('--no-squash', action='store_true',
4172 help='Don\'t squash multiple commits into one ' +
4173 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004174 parser.add_option('--topic', default=None,
4175 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004176 parser.add_option('--email', default=None,
4177 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004178 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4179 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004180 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4181 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004182 help='Send the patchset to do a CQ dry run right after '
4183 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004184 parser.add_option('--dependencies', action='store_true',
4185 help='Uploads CLs of all the local branches that depend on '
4186 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004187
rmistry@google.com2dd99862015-06-22 12:22:18 +00004188 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004189 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004190 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004191 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004192 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004193 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004194 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004195
sbc@chromium.org71437c02015-04-09 19:29:40 +00004196 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004197 return 1
4198
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004199 options.reviewers = cleanup_list(options.reviewers)
4200 options.cc = cleanup_list(options.cc)
4201
tandriib80458a2016-06-23 12:20:07 -07004202 if options.message_file:
4203 if options.message:
4204 parser.error('only one of --message and --message-file allowed.')
4205 options.message = gclient_utils.FileRead(options.message_file)
4206 options.message_file = None
4207
tandrii4d0545a2016-07-06 03:56:49 -07004208 if options.cq_dry_run and options.use_commit_queue:
4209 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4210
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004211 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4212 settings.GetIsGerrit()
4213
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004214 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004215 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004216
4217
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004218def IsSubmoduleMergeCommit(ref):
4219 # When submodules are added to the repo, we expect there to be a single
4220 # non-git-svn merge commit at remote HEAD with a signature comment.
4221 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004222 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004223 return RunGit(cmd) != ''
4224
4225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004227 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004229 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4230 upstream and closes the issue automatically and atomically.
4231
4232 Otherwise (in case of Rietveld):
4233 Squashes branch into a single commit.
4234 Updates changelog with metadata (e.g. pointer to review).
4235 Pushes/dcommits the code upstream.
4236 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237 """
4238 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4239 help='bypass upload presubmit hook')
4240 parser.add_option('-m', dest='message',
4241 help="override review description")
4242 parser.add_option('-f', action='store_true', dest='force',
4243 help="force yes to questions (don't prompt)")
4244 parser.add_option('-c', dest='contributor',
4245 help="external contributor for patch (appended to " +
4246 "description and used as author for git). Should be " +
4247 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004248 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004249 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004251 auth_config = auth.extract_auth_config_from_options(options)
4252
4253 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004254
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004255 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4256 if cl.IsGerrit():
4257 if options.message:
4258 # This could be implemented, but it requires sending a new patch to
4259 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4260 # Besides, Gerrit has the ability to change the commit message on submit
4261 # automatically, thus there is no need to support this option (so far?).
4262 parser.error('-m MESSAGE option is not supported for Gerrit.')
4263 if options.contributor:
4264 parser.error(
4265 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4266 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4267 'the contributor\'s "name <email>". If you can\'t upload such a '
4268 'commit for review, contact your repository admin and request'
4269 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004270 if not cl.GetIssue():
4271 DieWithError('You must upload the issue first to Gerrit.\n'
4272 ' If you would rather have `git cl land` upload '
4273 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004274 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4275 options.verbose)
4276
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004277 current = cl.GetBranch()
4278 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4279 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print()
4281 print('Attempting to push branch %r into another local branch!' % current)
4282 print()
4283 print('Either reparent this branch on top of origin/master:')
4284 print(' git reparent-branch --root')
4285 print()
4286 print('OR run `git rebase-update` if you think the parent branch is ')
4287 print('already committed.')
4288 print()
4289 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004290 return 1
4291
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004292 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293 # Default to merging against our best guess of the upstream branch.
4294 args = [cl.GetUpstreamBranch()]
4295
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004296 if options.contributor:
4297 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004298 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004299 return 1
4300
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004302 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303
sbc@chromium.org71437c02015-04-09 19:29:40 +00004304 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305 return 1
4306
4307 # This rev-list syntax means "show all commits not in my branch that
4308 # are in base_branch".
4309 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4310 base_branch]).splitlines()
4311 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print('Base branch "%s" has %d commits '
4313 'not in this branch.' % (base_branch, len(upstream_commits)))
4314 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315 return 1
4316
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004317 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004318 svn_head = None
4319 if cmd == 'dcommit' or base_has_submodules:
4320 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4321 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004323 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004324 # If the base_head is a submodule merge commit, the first parent of the
4325 # base_head should be a git-svn commit, which is what we're interested in.
4326 base_svn_head = base_branch
4327 if base_has_submodules:
4328 base_svn_head += '^1'
4329
4330 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004332 print('This branch has %d additional commits not upstreamed yet.'
4333 % len(extra_commits.splitlines()))
4334 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4335 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004336 return 1
4337
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004338 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004339 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004340 author = None
4341 if options.contributor:
4342 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004343 hook_results = cl.RunHook(
4344 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004345 may_prompt=not options.force,
4346 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004347 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004348 if not hook_results.should_continue():
4349 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004350
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004351 # Check the tree status if the tree status URL is set.
4352 status = GetTreeStatus()
4353 if 'closed' == status:
4354 print('The tree is closed. Please wait for it to reopen. Use '
4355 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4356 return 1
4357 elif 'unknown' == status:
4358 print('Unable to determine tree status. Please verify manually and '
4359 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4360 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004361
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004362 change_desc = ChangeDescription(options.message)
4363 if not change_desc.description and cl.GetIssue():
4364 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004365
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004366 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004367 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004368 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004369 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004370 print('No description set.')
4371 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004372 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004374 # Keep a separate copy for the commit message, because the commit message
4375 # contains the link to the Rietveld issue, while the Rietveld message contains
4376 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004377 # Keep a separate copy for the commit message.
4378 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004379 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004380
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004381 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004382 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004383 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004384 # after it. Add a period on a new line to circumvent this. Also add a space
4385 # before the period to make sure that Gitiles continues to correctly resolve
4386 # the URL.
4387 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004389 commit_desc.append_footer('Patch from %s.' % options.contributor)
4390
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004391 print('Description:')
4392 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004393
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004394 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004396 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004398 # We want to squash all this branch's commits into one commit with the proper
4399 # description. We do this by doing a "reset --soft" to the base branch (which
4400 # keeps the working copy the same), then dcommitting that. If origin/master
4401 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4402 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004404 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4405 # Delete the branches if they exist.
4406 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4407 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4408 result = RunGitWithCode(showref_cmd)
4409 if result[0] == 0:
4410 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411
4412 # We might be in a directory that's present in this branch but not in the
4413 # trunk. Move up to the top of the tree so that git commands that expect a
4414 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004415 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004416 if rel_base_path:
4417 os.chdir(rel_base_path)
4418
4419 # Stuff our change into the merge branch.
4420 # We wrap in a try...finally block so if anything goes wrong,
4421 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004422 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004423 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004424 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004425 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004427 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004428 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004429 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004430 RunGit(
4431 [
4432 'commit', '--author', options.contributor,
4433 '-m', commit_desc.description,
4434 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004436 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004437 if base_has_submodules:
4438 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4439 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4440 RunGit(['checkout', CHERRY_PICK_BRANCH])
4441 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004442 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004443 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004444 mirror = settings.GetGitMirror(remote)
4445 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004446 pending_prefix = settings.GetPendingRefPrefix()
4447 if not pending_prefix or branch.startswith(pending_prefix):
4448 # If not using refs/pending/heads/* at all, or target ref is already set
4449 # to pending, then push to the target ref directly.
4450 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004451 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004452 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004453 else:
4454 # Cherry-pick the change on top of pending ref and then push it.
4455 assert branch.startswith('refs/'), branch
4456 assert pending_prefix[-1] == '/', pending_prefix
4457 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004458 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004459 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004460 if retcode == 0:
4461 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462 else:
4463 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004464 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004465 'svn', 'dcommit',
4466 '-C%s' % options.similarity,
4467 '--no-rebase', '--rmdir',
4468 ]
4469 if settings.GetForceHttpsCommitUrl():
4470 # Allow forcing https commit URLs for some projects that don't allow
4471 # committing to http URLs (like Google Code).
4472 remote_url = cl.GetGitSvnRemoteUrl()
4473 if urlparse.urlparse(remote_url).scheme == 'http':
4474 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004475 cmd_args.append('--commit-url=%s' % remote_url)
4476 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004477 if 'Committed r' in output:
4478 revision = re.match(
4479 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4480 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004481 finally:
4482 # And then swap back to the original branch and clean up.
4483 RunGit(['checkout', '-q', cl.GetBranch()])
4484 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004485 if base_has_submodules:
4486 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004487
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004488 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004489 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004490 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004491
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004492 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004493 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004494 try:
4495 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4496 # We set pushed_to_pending to False, since it made it all the way to the
4497 # real ref.
4498 pushed_to_pending = False
4499 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004500 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004502 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004503 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004505 if not to_pending:
4506 if viewvc_url and revision:
4507 change_desc.append_footer(
4508 'Committed: %s%s' % (viewvc_url, revision))
4509 elif revision:
4510 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004511 print('Closing issue '
4512 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004513 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004514 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004515 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004516 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004517 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004518 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004519 if options.bypass_hooks:
4520 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4521 else:
4522 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004523 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004524
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004525 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004526 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004527 print('The commit is in the pending queue (%s).' % pending_ref)
4528 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4529 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004530
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004531 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4532 if os.path.isfile(hook):
4533 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004534
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004535 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536
4537
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004538def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004539 print()
4540 print('Waiting for commit to be landed on %s...' % real_ref)
4541 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004542 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4543 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004544 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004545
4546 loop = 0
4547 while True:
4548 sys.stdout.write('fetching (%d)... \r' % loop)
4549 sys.stdout.flush()
4550 loop += 1
4551
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004552 if mirror:
4553 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004554 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4555 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4556 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4557 for commit in commits.splitlines():
4558 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004559 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004560 return commit
4561
4562 current_rev = to_rev
4563
4564
tandriibf429402016-09-14 07:09:12 -07004565def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004566 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4567
4568 Returns:
4569 (retcode of last operation, output log of last operation).
4570 """
4571 assert pending_ref.startswith('refs/'), pending_ref
4572 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4573 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4574 code = 0
4575 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004576 max_attempts = 3
4577 attempts_left = max_attempts
4578 while attempts_left:
4579 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004580 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004581 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004582
4583 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004584 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004585 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004586 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004587 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004588 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004589 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004590 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004591 continue
4592
4593 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004594 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004595 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004596 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004597 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004598 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4599 'the following files have merge conflicts:' % pending_ref)
4600 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4601 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004602 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004603 return code, out
4604
4605 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004606 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004607 code, out = RunGitWithCode(
4608 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4609 if code == 0:
4610 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004611 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004612 return code, out
4613
vapiera7fbd5a2016-06-16 09:17:49 -07004614 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004615 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004616 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004617 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004618 print('Fatal push error. Make sure your .netrc credentials and git '
4619 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004620 return code, out
4621
vapiera7fbd5a2016-06-16 09:17:49 -07004622 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004623 return code, out
4624
4625
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004626def IsFatalPushFailure(push_stdout):
4627 """True if retrying push won't help."""
4628 return '(prohibited by Gerrit)' in push_stdout
4629
4630
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004631@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004632def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004633 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004634 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004635 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004636 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004637 message = """This repository appears to be a git-svn mirror, but we
4638don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004639 else:
4640 message = """This doesn't appear to be an SVN repository.
4641If your project has a true, writeable git repository, you probably want to run
4642'git cl land' instead.
4643If your project has a git mirror of an upstream SVN master, you probably need
4644to run 'git svn init'.
4645
4646Using the wrong command might cause your commit to appear to succeed, and the
4647review to be closed, without actually landing upstream. If you choose to
4648proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004649 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004650 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004651 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4652 'Please let us know of this project you are committing to:'
4653 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654 return SendUpstream(parser, args, 'dcommit')
4655
4656
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004657@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004658def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004659 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004660 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004661 print('This appears to be an SVN repository.')
4662 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004663 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004664 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004665 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004666
4667
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004668@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004669def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004670 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004671 parser.add_option('-b', dest='newbranch',
4672 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004673 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004674 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004675 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4676 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004677 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004678 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004679 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004680 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004682 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004683
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004684
4685 group = optparse.OptionGroup(
4686 parser,
4687 'Options for continuing work on the current issue uploaded from a '
4688 'different clone (e.g. different machine). Must be used independently '
4689 'from the other options. No issue number should be specified, and the '
4690 'branch must have an issue number associated with it')
4691 group.add_option('--reapply', action='store_true', dest='reapply',
4692 help='Reset the branch and reapply the issue.\n'
4693 'CAUTION: This will undo any local changes in this '
4694 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004695
4696 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004697 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004698 parser.add_option_group(group)
4699
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004700 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004701 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004702 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004703 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004704 auth_config = auth.extract_auth_config_from_options(options)
4705
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004706
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004707 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004708 if options.newbranch:
4709 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004710 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004711 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004712
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004713 cl = Changelist(auth_config=auth_config,
4714 codereview=options.forced_codereview)
4715 if not cl.GetIssue():
4716 parser.error('current branch must have an associated issue')
4717
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004718 upstream = cl.GetUpstreamBranch()
4719 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004720 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004721
4722 RunGit(['reset', '--hard', upstream])
4723 if options.pull:
4724 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004725
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004726 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4727 options.directory)
4728
4729 if len(args) != 1 or not args[0]:
4730 parser.error('Must specify issue number or url')
4731
4732 # We don't want uncommitted changes mixed up with the patch.
4733 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004734 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004735
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004736 if options.newbranch:
4737 if options.force:
4738 RunGit(['branch', '-D', options.newbranch],
4739 stderr=subprocess2.PIPE, error_ok=True)
4740 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004741 elif not GetCurrentBranch():
4742 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004743
4744 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4745
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004746 if cl.IsGerrit():
4747 if options.reject:
4748 parser.error('--reject is not supported with Gerrit codereview.')
4749 if options.nocommit:
4750 parser.error('--nocommit is not supported with Gerrit codereview.')
4751 if options.directory:
4752 parser.error('--directory is not supported with Gerrit codereview.')
4753
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004754 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004755 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756
4757
4758def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004759 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760 # Provide a wrapper for git svn rebase to help avoid accidental
4761 # git svn dcommit.
4762 # It's the only command that doesn't use parser at all since we just defer
4763 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004764
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004765 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004766
4767
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004768def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004769 """Fetches the tree status and returns either 'open', 'closed',
4770 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004771 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772 if url:
4773 status = urllib2.urlopen(url).read().lower()
4774 if status.find('closed') != -1 or status == '0':
4775 return 'closed'
4776 elif status.find('open') != -1 or status == '1':
4777 return 'open'
4778 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004779 return 'unset'
4780
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004782def GetTreeStatusReason():
4783 """Fetches the tree status from a json url and returns the message
4784 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004785 url = settings.GetTreeStatusUrl()
4786 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004787 connection = urllib2.urlopen(json_url)
4788 status = json.loads(connection.read())
4789 connection.close()
4790 return status['message']
4791
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004793def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004794 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004795 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004796 status = GetTreeStatus()
4797 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004798 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004799 return 2
4800
vapiera7fbd5a2016-06-16 09:17:49 -07004801 print('The tree is %s' % status)
4802 print()
4803 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004804 if status != 'open':
4805 return 1
4806 return 0
4807
4808
maruel@chromium.org15192402012-09-06 12:38:29 +00004809def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004810 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004811 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004812 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004813 '-b', '--bot', action='append',
4814 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4815 'times to specify multiple builders. ex: '
4816 '"-b win_rel -b win_layout". See '
4817 'the try server waterfall for the builders name and the tests '
4818 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004819 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004820 '-B', '--bucket', default='',
4821 help=('Buildbucket bucket to send the try requests.'))
4822 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004823 '-m', '--master', default='',
4824 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004825 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004826 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004827 help='Revision to use for the try job; default: the revision will '
4828 'be determined by the try recipe that builder runs, which usually '
4829 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004830 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004831 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004832 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004833 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004834 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004835 '--project',
4836 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004837 'in recipe to determine to which repository or directory to '
4838 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004839 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004840 '-p', '--property', dest='properties', action='append', default=[],
4841 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004842 'key2=value2 etc. The value will be treated as '
4843 'json if decodable, or as string otherwise. '
4844 'NOTE: using this may make your try job not usable for CQ, '
4845 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004846 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004847 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4848 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004849 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004850 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004851 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004852 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004853
machenbach@chromium.org45453142015-09-15 08:45:22 +00004854 # Make sure that all properties are prop=value pairs.
4855 bad_params = [x for x in options.properties if '=' not in x]
4856 if bad_params:
4857 parser.error('Got properties with missing "=": %s' % bad_params)
4858
maruel@chromium.org15192402012-09-06 12:38:29 +00004859 if args:
4860 parser.error('Unknown arguments: %s' % args)
4861
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004862 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004863 if not cl.GetIssue():
4864 parser.error('Need to upload first')
4865
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004866 if cl.IsGerrit():
4867 parser.error(
4868 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4869 'If your project has Commit Queue, dry run is a workaround:\n'
4870 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004871
tandriie113dfd2016-10-11 10:20:12 -07004872 error_message = cl.CannotTriggerTryJobReason()
4873 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004874 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004875
borenet6c0efe62016-10-19 08:13:29 -07004876 if options.bucket and options.master:
4877 parser.error('Only one of --bucket and --master may be used.')
4878
qyearsley1fdfcb62016-10-24 13:22:03 -07004879 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004880
qyearsley1fdfcb62016-10-24 13:22:03 -07004881 if not buckets:
4882 # Default to triggering Dry Run (see http://crbug.com/625697).
4883 if options.verbose:
4884 print('git cl try with no bots now defaults to CQ Dry Run.')
4885 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004886
borenet6c0efe62016-10-19 08:13:29 -07004887 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004888 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004889 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004890 'of bot requires an initial job from a parent (usually a builder). '
4891 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004892 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004893 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004894
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004895 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004896 if patchset != cl.GetPatchset():
4897 print('Warning: Codereview server has newer patchsets (%s) than most '
4898 'recent upload from local checkout (%s). Did a previous upload '
4899 'fail?\n'
4900 'By default, git cl try uses the latest patchset from '
4901 'codereview, continuing to use patchset %s.\n' %
4902 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004903
tandrii568043b2016-10-11 07:49:18 -07004904 try:
borenet6c0efe62016-10-19 08:13:29 -07004905 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4906 patchset)
tandrii568043b2016-10-11 07:49:18 -07004907 except BuildbucketResponseException as ex:
4908 print('ERROR: %s' % ex)
4909 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004910 return 0
4911
4912
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004913def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004914 """Prints info about try jobs associated with current CL."""
4915 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004916 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004917 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004918 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004919 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004920 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004921 '--color', action='store_true', default=setup_color.IS_TTY,
4922 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004923 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004924 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4925 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004926 group.add_option(
4927 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004928 parser.add_option_group(group)
4929 auth.add_auth_options(parser)
4930 options, args = parser.parse_args(args)
4931 if args:
4932 parser.error('Unrecognized args: %s' % ' '.join(args))
4933
4934 auth_config = auth.extract_auth_config_from_options(options)
4935 cl = Changelist(auth_config=auth_config)
4936 if not cl.GetIssue():
4937 parser.error('Need to upload first')
4938
tandrii221ab252016-10-06 08:12:04 -07004939 patchset = options.patchset
4940 if not patchset:
4941 patchset = cl.GetMostRecentPatchset()
4942 if not patchset:
4943 parser.error('Codereview doesn\'t know about issue %s. '
4944 'No access to issue or wrong issue number?\n'
4945 'Either upload first, or pass --patchset explicitely' %
4946 cl.GetIssue())
4947
4948 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004949 print('Warning: Codereview server has newer patchsets (%s) than most '
4950 'recent upload from local checkout (%s). Did a previous upload '
4951 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004952 'By default, git cl try-results uses the latest patchset from '
4953 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004954 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004955 try:
tandrii221ab252016-10-06 08:12:04 -07004956 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004957 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004958 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004959 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004960 if options.json:
4961 write_try_results_json(options.json, jobs)
4962 else:
4963 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004964 return 0
4965
4966
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004967@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004968def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004969 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004970 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004971 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004972 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004974 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004975 if args:
4976 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004977 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004978 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004979 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004980 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004981
4982 # Clear configured merge-base, if there is one.
4983 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004984 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004985 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004986 return 0
4987
4988
thestig@chromium.org00858c82013-12-02 23:08:03 +00004989def CMDweb(parser, args):
4990 """Opens the current CL in the web browser."""
4991 _, args = parser.parse_args(args)
4992 if args:
4993 parser.error('Unrecognized args: %s' % ' '.join(args))
4994
4995 issue_url = Changelist().GetIssueURL()
4996 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004997 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004998 return 1
4999
5000 webbrowser.open(issue_url)
5001 return 0
5002
5003
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005004def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005005 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005006 parser.add_option('-d', '--dry-run', action='store_true',
5007 help='trigger in dry run mode')
5008 parser.add_option('-c', '--clear', action='store_true',
5009 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005010 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005011 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005012 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005013 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005014 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005015 if args:
5016 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005017 if options.dry_run and options.clear:
5018 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5019
iannuccie53c9352016-08-17 14:40:40 -07005020 cl = Changelist(auth_config=auth_config, issue=options.issue,
5021 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005022 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005023 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005024 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005025 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005026 state = _CQState.DRY_RUN
5027 else:
5028 state = _CQState.COMMIT
5029 if not cl.GetIssue():
5030 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005031 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005032 return 0
5033
5034
groby@chromium.org411034a2013-02-26 15:12:01 +00005035def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005036 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005037 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005038 auth.add_auth_options(parser)
5039 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005040 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005041 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005042 if args:
5043 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005044 cl = Changelist(auth_config=auth_config, issue=options.issue,
5045 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005046 # Ensure there actually is an issue to close.
5047 cl.GetDescription()
5048 cl.CloseIssue()
5049 return 0
5050
5051
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005052def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005053 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005054 parser.add_option(
5055 '--stat',
5056 action='store_true',
5057 dest='stat',
5058 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005059 auth.add_auth_options(parser)
5060 options, args = parser.parse_args(args)
5061 auth_config = auth.extract_auth_config_from_options(options)
5062 if args:
5063 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005064
5065 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005066 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005067 # Staged changes would be committed along with the patch from last
5068 # upload, hence counted toward the "last upload" side in the final
5069 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005070 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005071 return 1
5072
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005073 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005074 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005075 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005076 if not issue:
5077 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005078 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005079 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005080
5081 # Create a new branch based on the merge-base
5082 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005083 # Clear cached branch in cl object, to avoid overwriting original CL branch
5084 # properties.
5085 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005086 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005087 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005088 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005089 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005090 return rtn
5091
wychen@chromium.org06928532015-02-03 02:11:29 +00005092 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005093 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005094 cmd = ['git', 'diff']
5095 if options.stat:
5096 cmd.append('--stat')
5097 cmd.extend([TMP_BRANCH, branch, '--'])
5098 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005099 finally:
5100 RunGit(['checkout', '-q', branch])
5101 RunGit(['branch', '-D', TMP_BRANCH])
5102
5103 return 0
5104
5105
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005106def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005107 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005108 parser.add_option(
5109 '--no-color',
5110 action='store_true',
5111 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005112 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005113 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005114 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005115
5116 author = RunGit(['config', 'user.email']).strip() or None
5117
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005118 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005119
5120 if args:
5121 if len(args) > 1:
5122 parser.error('Unknown args')
5123 base_branch = args[0]
5124 else:
5125 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005126 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005127
5128 change = cl.GetChange(base_branch, None)
5129 return owners_finder.OwnersFinder(
5130 [f.LocalPath() for f in
5131 cl.GetChange(base_branch, None).AffectedFiles()],
5132 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005133 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005134 disable_color=options.no_color).run()
5135
5136
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005137def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005138 """Generates a diff command."""
5139 # Generate diff for the current branch's changes.
5140 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5141 upstream_commit, '--' ]
5142
5143 if args:
5144 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005145 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005146 diff_cmd.append(arg)
5147 else:
5148 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005149
5150 return diff_cmd
5151
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005152def MatchingFileType(file_name, extensions):
5153 """Returns true if the file name ends with one of the given extensions."""
5154 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005155
enne@chromium.org555cfe42014-01-29 18:21:39 +00005156@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005157def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005158 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005159 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005160 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005161 parser.add_option('--full', action='store_true',
5162 help='Reformat the full content of all touched files')
5163 parser.add_option('--dry-run', action='store_true',
5164 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005165 parser.add_option('--python', action='store_true',
5166 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005167 parser.add_option('--diff', action='store_true',
5168 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005169 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005170
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005171 # git diff generates paths against the root of the repository. Change
5172 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005173 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005174 if rel_base_path:
5175 os.chdir(rel_base_path)
5176
digit@chromium.org29e47272013-05-17 17:01:46 +00005177 # Grab the merge-base commit, i.e. the upstream commit of the current
5178 # branch when it was created or the last time it was rebased. This is
5179 # to cover the case where the user may have called "git fetch origin",
5180 # moving the origin branch to a newer commit, but hasn't rebased yet.
5181 upstream_commit = None
5182 cl = Changelist()
5183 upstream_branch = cl.GetUpstreamBranch()
5184 if upstream_branch:
5185 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5186 upstream_commit = upstream_commit.strip()
5187
5188 if not upstream_commit:
5189 DieWithError('Could not find base commit for this branch. '
5190 'Are you in detached state?')
5191
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005192 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5193 diff_output = RunGit(changed_files_cmd)
5194 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005195 # Filter out files deleted by this CL
5196 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005197
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005198 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5199 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5200 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005201 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005202
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005203 top_dir = os.path.normpath(
5204 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5205
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005206 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5207 # formatted. This is used to block during the presubmit.
5208 return_value = 0
5209
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005210 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005211 # Locate the clang-format binary in the checkout
5212 try:
5213 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005214 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005215 DieWithError(e)
5216
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005217 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005218 cmd = [clang_format_tool]
5219 if not opts.dry_run and not opts.diff:
5220 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005221 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005222 if opts.diff:
5223 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005224 else:
5225 env = os.environ.copy()
5226 env['PATH'] = str(os.path.dirname(clang_format_tool))
5227 try:
5228 script = clang_format.FindClangFormatScriptInChromiumTree(
5229 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005230 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005231 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005232
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005233 cmd = [sys.executable, script, '-p0']
5234 if not opts.dry_run and not opts.diff:
5235 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005236
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005237 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5238 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005239
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005240 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5241 if opts.diff:
5242 sys.stdout.write(stdout)
5243 if opts.dry_run and len(stdout) > 0:
5244 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005245
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005246 # Similar code to above, but using yapf on .py files rather than clang-format
5247 # on C/C++ files
5248 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005249 yapf_tool = gclient_utils.FindExecutable('yapf')
5250 if yapf_tool is None:
5251 DieWithError('yapf not found in PATH')
5252
5253 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005254 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005255 cmd = [yapf_tool]
5256 if not opts.dry_run and not opts.diff:
5257 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005258 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005259 if opts.diff:
5260 sys.stdout.write(stdout)
5261 else:
5262 # TODO(sbc): yapf --lines mode still has some issues.
5263 # https://github.com/google/yapf/issues/154
5264 DieWithError('--python currently only works with --full')
5265
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005266 # Dart's formatter does not have the nice property of only operating on
5267 # modified chunks, so hard code full.
5268 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005269 try:
5270 command = [dart_format.FindDartFmtToolInChromiumTree()]
5271 if not opts.dry_run and not opts.diff:
5272 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005273 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005274
ppi@chromium.org6593d932016-03-03 15:41:15 +00005275 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005276 if opts.dry_run and stdout:
5277 return_value = 2
5278 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005279 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5280 'found in this checkout. Files in other languages are still '
5281 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005282
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005283 # Format GN build files. Always run on full build files for canonical form.
5284 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005285 cmd = ['gn', 'format' ]
5286 if opts.dry_run or opts.diff:
5287 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005288 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005289 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5290 shell=sys.platform == 'win32',
5291 cwd=top_dir)
5292 if opts.dry_run and gn_ret == 2:
5293 return_value = 2 # Not formatted.
5294 elif opts.diff and gn_ret == 2:
5295 # TODO this should compute and print the actual diff.
5296 print("This change has GN build file diff for " + gn_diff_file)
5297 elif gn_ret != 0:
5298 # For non-dry run cases (and non-2 return values for dry-run), a
5299 # nonzero error code indicates a failure, probably because the file
5300 # doesn't parse.
5301 DieWithError("gn format failed on " + gn_diff_file +
5302 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005303
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005304 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005305
5306
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005307@subcommand.usage('<codereview url or issue id>')
5308def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005309 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005310 _, args = parser.parse_args(args)
5311
5312 if len(args) != 1:
5313 parser.print_help()
5314 return 1
5315
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005316 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005317 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005318 parser.print_help()
5319 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005320 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005321
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005322 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005323 output = RunGit(['config', '--local', '--get-regexp',
5324 r'branch\..*\.%s' % issueprefix],
5325 error_ok=True)
5326 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005327 if issue == target_issue:
5328 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005329
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005330 branches = []
5331 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005332 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005333 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005334 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005335 return 1
5336 if len(branches) == 1:
5337 RunGit(['checkout', branches[0]])
5338 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005339 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005340 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005341 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005342 which = raw_input('Choose by index: ')
5343 try:
5344 RunGit(['checkout', branches[int(which)]])
5345 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005346 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005347 return 1
5348
5349 return 0
5350
5351
maruel@chromium.org29404b52014-09-08 22:58:00 +00005352def CMDlol(parser, args):
5353 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005354 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005355 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5356 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5357 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005358 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005359 return 0
5360
5361
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005362class OptionParser(optparse.OptionParser):
5363 """Creates the option parse and add --verbose support."""
5364 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005365 optparse.OptionParser.__init__(
5366 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005367 self.add_option(
5368 '-v', '--verbose', action='count', default=0,
5369 help='Use 2 times for more debugging info')
5370
5371 def parse_args(self, args=None, values=None):
5372 options, args = optparse.OptionParser.parse_args(self, args, values)
5373 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5374 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5375 return options, args
5376
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005378def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005379 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005380 print('\nYour python version %s is unsupported, please upgrade.\n' %
5381 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005382 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005383
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005384 # Reload settings.
5385 global settings
5386 settings = Settings()
5387
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005388 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005389 dispatcher = subcommand.CommandDispatcher(__name__)
5390 try:
5391 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005392 except auth.AuthenticationError as e:
5393 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005394 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005395 if e.code != 500:
5396 raise
5397 DieWithError(
5398 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5399 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005400 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005401
5402
5403if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005404 # These affect sys.stdout so do it outside of main() to simplify mocks in
5405 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005406 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005407 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005408 try:
5409 sys.exit(main(sys.argv[1:]))
5410 except KeyboardInterrupt:
5411 sys.stderr.write('interrupted\n')
5412 sys.exit(1)