blob: 09132a1b5be5a61cff2c5e03af977066cd640a7d [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,
512 'reason': options.name,
tandriide281ae2016-10-12 06:02:30 -0700513 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000514 },
515 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000516 if 'presubmit' in builder.lower():
517 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000518 if tests:
519 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700520 if extra_properties:
521 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000522 if options.clobber:
523 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700524
525 tags = [
526 'builder:%s' % builder,
527 'buildset:%s' % buildset,
528 'user_agent:git_cl_try',
529 ]
530 if master:
531 parameters['properties']['master'] = master
532 tags.append('master:%s' % master)
533
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000534 batch_req_body['builds'].append(
535 {
536 'bucket': bucket,
537 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700539 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000540 }
541 )
542
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700544 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 http,
546 buildbucket_put_url,
547 'PUT',
548 body=json.dumps(batch_req_body),
549 headers={'Content-Type': 'application/json'}
550 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000551 print_text.append('To see results here, run: git cl try-results')
552 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700553 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000554
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000555
tandrii221ab252016-10-06 08:12:04 -0700556def fetch_try_jobs(auth_config, changelist, buildbucket_host,
557 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700558 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559
qyearsley53f48a12016-09-01 10:45:13 -0700560 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 """
tandrii221ab252016-10-06 08:12:04 -0700562 assert buildbucket_host
563 assert changelist.GetIssue(), 'CL must be uploaded first'
564 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
565 patchset = patchset or changelist.GetMostRecentPatchset()
566 assert patchset, 'CL must be uploaded first'
567
568 codereview_url = changelist.GetCodereviewServer()
569 codereview_host = urlparse.urlparse(codereview_url).hostname
570 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 if authenticator.has_cached_credentials():
572 http = authenticator.authorize(httplib2.Http())
573 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700574 print('Warning: Some results might be missing because %s' %
575 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700576 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 http = httplib2.Http()
578
579 http.force_exception_to_status_code = True
580
tandrii221ab252016-10-06 08:12:04 -0700581 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
582 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
583 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700585 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000586 params = {'tag': 'buildset:%s' % buildset}
587
588 builds = {}
589 while True:
590 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700591 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700593 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 for build in content.get('builds', []):
595 builds[build['id']] = build
596 if 'next_cursor' in content:
597 params['start_cursor'] = content['next_cursor']
598 else:
599 break
600 return builds
601
602
qyearsleyeab3c042016-08-24 09:18:28 -0700603def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000604 """Prints nicely result of fetch_try_jobs."""
605 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700606 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000607 return
608
609 # Make a copy, because we'll be modifying builds dictionary.
610 builds = builds.copy()
611 builder_names_cache = {}
612
613 def get_builder(b):
614 try:
615 return builder_names_cache[b['id']]
616 except KeyError:
617 try:
618 parameters = json.loads(b['parameters_json'])
619 name = parameters['builder_name']
620 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700621 print('WARNING: failed to get builder name for build %s: %s' % (
622 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000623 name = None
624 builder_names_cache[b['id']] = name
625 return name
626
627 def get_bucket(b):
628 bucket = b['bucket']
629 if bucket.startswith('master.'):
630 return bucket[len('master.'):]
631 return bucket
632
633 if options.print_master:
634 name_fmt = '%%-%ds %%-%ds' % (
635 max(len(str(get_bucket(b))) for b in builds.itervalues()),
636 max(len(str(get_builder(b))) for b in builds.itervalues()))
637 def get_name(b):
638 return name_fmt % (get_bucket(b), get_builder(b))
639 else:
640 name_fmt = '%%-%ds' % (
641 max(len(str(get_builder(b))) for b in builds.itervalues()))
642 def get_name(b):
643 return name_fmt % get_builder(b)
644
645 def sort_key(b):
646 return b['status'], b.get('result'), get_name(b), b.get('url')
647
648 def pop(title, f, color=None, **kwargs):
649 """Pop matching builds from `builds` dict and print them."""
650
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000651 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000652 colorize = str
653 else:
654 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
655
656 result = []
657 for b in builds.values():
658 if all(b.get(k) == v for k, v in kwargs.iteritems()):
659 builds.pop(b['id'])
660 result.append(b)
661 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700662 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000663 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700664 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000665
666 total = len(builds)
667 pop(status='COMPLETED', result='SUCCESS',
668 title='Successes:', color=Fore.GREEN,
669 f=lambda b: (get_name(b), b.get('url')))
670 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
671 title='Infra Failures:', color=Fore.MAGENTA,
672 f=lambda b: (get_name(b), b.get('url')))
673 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
674 title='Failures:', color=Fore.RED,
675 f=lambda b: (get_name(b), b.get('url')))
676 pop(status='COMPLETED', result='CANCELED',
677 title='Canceled:', color=Fore.MAGENTA,
678 f=lambda b: (get_name(b),))
679 pop(status='COMPLETED', result='FAILURE',
680 failure_reason='INVALID_BUILD_DEFINITION',
681 title='Wrong master/builder name:', color=Fore.MAGENTA,
682 f=lambda b: (get_name(b),))
683 pop(status='COMPLETED', result='FAILURE',
684 title='Other failures:',
685 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
686 pop(status='COMPLETED',
687 title='Other finished:',
688 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
689 pop(status='STARTED',
690 title='Started:', color=Fore.YELLOW,
691 f=lambda b: (get_name(b), b.get('url')))
692 pop(status='SCHEDULED',
693 title='Scheduled:',
694 f=lambda b: (get_name(b), 'id=%s' % b['id']))
695 # The last section is just in case buildbucket API changes OR there is a bug.
696 pop(title='Other:',
697 f=lambda b: (get_name(b), 'id=%s' % b['id']))
698 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700699 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000700
701
qyearsley53f48a12016-09-01 10:45:13 -0700702def write_try_results_json(output_file, builds):
703 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
704
705 The input |builds| dict is assumed to be generated by Buildbucket.
706 Buildbucket documentation: http://goo.gl/G0s101
707 """
708
709 def convert_build_dict(build):
710 return {
711 'buildbucket_id': build.get('id'),
712 'status': build.get('status'),
713 'result': build.get('result'),
714 'bucket': build.get('bucket'),
715 'builder_name': json.loads(
716 build.get('parameters_json', '{}')).get('builder_name'),
717 'failure_reason': build.get('failure_reason'),
718 'url': build.get('url'),
719 }
720
721 converted = []
722 for _, build in sorted(builds.items()):
723 converted.append(convert_build_dict(build))
724 write_json(output_file, converted)
725
726
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000727def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
728 """Return the corresponding git ref if |base_url| together with |glob_spec|
729 matches the full |url|.
730
731 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
732 """
733 fetch_suburl, as_ref = glob_spec.split(':')
734 if allow_wildcards:
735 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
736 if glob_match:
737 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
738 # "branches/{472,597,648}/src:refs/remotes/svn/*".
739 branch_re = re.escape(base_url)
740 if glob_match.group(1):
741 branch_re += '/' + re.escape(glob_match.group(1))
742 wildcard = glob_match.group(2)
743 if wildcard == '*':
744 branch_re += '([^/]*)'
745 else:
746 # Escape and replace surrounding braces with parentheses and commas
747 # with pipe symbols.
748 wildcard = re.escape(wildcard)
749 wildcard = re.sub('^\\\\{', '(', wildcard)
750 wildcard = re.sub('\\\\,', '|', wildcard)
751 wildcard = re.sub('\\\\}$', ')', wildcard)
752 branch_re += wildcard
753 if glob_match.group(3):
754 branch_re += re.escape(glob_match.group(3))
755 match = re.match(branch_re, url)
756 if match:
757 return re.sub('\*$', match.group(1), as_ref)
758
759 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
760 if fetch_suburl:
761 full_url = base_url + '/' + fetch_suburl
762 else:
763 full_url = base_url
764 if full_url == url:
765 return as_ref
766 return None
767
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000768
iannucci@chromium.org79540052012-10-19 23:15:26 +0000769def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000770 """Prints statistics about the change to the user."""
771 # --no-ext-diff is broken in some versions of Git, so try to work around
772 # this by overriding the environment (but there is still a problem if the
773 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000774 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000775 if 'GIT_EXTERNAL_DIFF' in env:
776 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000777
778 if find_copies:
779 similarity_options = ['--find-copies-harder', '-l100000',
780 '-C%s' % similarity]
781 else:
782 similarity_options = ['-M%s' % similarity]
783
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000784 try:
785 stdout = sys.stdout.fileno()
786 except AttributeError:
787 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000788 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000789 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000790 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000791 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000792
793
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000794class BuildbucketResponseException(Exception):
795 pass
796
797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798class Settings(object):
799 def __init__(self):
800 self.default_server = None
801 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000802 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 self.is_git_svn = None
804 self.svn_branch = None
805 self.tree_status_url = None
806 self.viewvc_url = None
807 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000808 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000809 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000810 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000811 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000812 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000813 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000814 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700815 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816
817 def LazyUpdateIfNeeded(self):
818 """Updates the settings from a codereview.settings file, if available."""
819 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000820 # The only value that actually changes the behavior is
821 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000822 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000823 error_ok=True
824 ).strip().lower()
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000827 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 LoadCodereviewSettingsFromFile(cr_settings_file)
829 self.updated = True
830
831 def GetDefaultServerUrl(self, error_ok=False):
832 if not self.default_server:
833 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000834 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 if error_ok:
837 return self.default_server
838 if not self.default_server:
839 error_message = ('Could not find settings file. You must configure '
840 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000841 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 return self.default_server
844
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000845 @staticmethod
846 def GetRelativeRoot():
847 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000850 if self.root is None:
851 self.root = os.path.abspath(self.GetRelativeRoot())
852 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000854 def GetGitMirror(self, remote='origin'):
855 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000856 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000857 if not os.path.isdir(local_url):
858 return None
859 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
860 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
861 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
862 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
863 if mirror.exists():
864 return mirror
865 return None
866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 def GetIsGitSvn(self):
868 """Return true if this repo looks like it's using git-svn."""
869 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000870 if self.GetPendingRefPrefix():
871 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
872 self.is_git_svn = False
873 else:
874 # If you have any "svn-remote.*" config keys, we think you're using svn.
875 self.is_git_svn = RunGitWithCode(
876 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 return self.is_git_svn
878
879 def GetSVNBranch(self):
880 if self.svn_branch is None:
881 if not self.GetIsGitSvn():
882 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
883
884 # Try to figure out which remote branch we're based on.
885 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000886 # 1) iterate through our branch history and find the svn URL.
887 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
889 # regexp matching the git-svn line that contains the URL.
890 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
891
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000892 # We don't want to go through all of history, so read a line from the
893 # pipe at a time.
894 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000895 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000896 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
897 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000898 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000899 for line in proc.stdout:
900 match = git_svn_re.match(line)
901 if match:
902 url = match.group(1)
903 proc.stdout.close() # Cut pipe.
904 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000906 if url:
907 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
908 remotes = RunGit(['config', '--get-regexp',
909 r'^svn-remote\..*\.url']).splitlines()
910 for remote in remotes:
911 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000913 remote = match.group(1)
914 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000915 rewrite_root = RunGit(
916 ['config', 'svn-remote.%s.rewriteRoot' % remote],
917 error_ok=True).strip()
918 if rewrite_root:
919 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000920 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000921 ['config', 'svn-remote.%s.fetch' % remote],
922 error_ok=True).strip()
923 if fetch_spec:
924 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
925 if self.svn_branch:
926 break
927 branch_spec = RunGit(
928 ['config', 'svn-remote.%s.branches' % remote],
929 error_ok=True).strip()
930 if branch_spec:
931 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
932 if self.svn_branch:
933 break
934 tag_spec = RunGit(
935 ['config', 'svn-remote.%s.tags' % remote],
936 error_ok=True).strip()
937 if tag_spec:
938 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
939 if self.svn_branch:
940 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941
942 if not self.svn_branch:
943 DieWithError('Can\'t guess svn branch -- try specifying it on the '
944 'command line')
945
946 return self.svn_branch
947
948 def GetTreeStatusUrl(self, error_ok=False):
949 if not self.tree_status_url:
950 error_message = ('You must configure your tree status URL by running '
951 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000952 self.tree_status_url = self._GetRietveldConfig(
953 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 return self.tree_status_url
955
956 def GetViewVCUrl(self):
957 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000958 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 return self.viewvc_url
960
rmistry@google.com90752582014-01-14 21:04:50 +0000961 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000962 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000963
rmistry@google.com78948ed2015-07-08 23:09:57 +0000964 def GetIsSkipDependencyUpload(self, branch_name):
965 """Returns true if specified branch should skip dep uploads."""
966 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
967 error_ok=True)
968
rmistry@google.com5626a922015-02-26 14:03:30 +0000969 def GetRunPostUploadHook(self):
970 run_post_upload_hook = self._GetRietveldConfig(
971 'run-post-upload-hook', error_ok=True)
972 return run_post_upload_hook == "True"
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000975 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000977 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000978 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000979
ukai@chromium.orge8077812012-02-03 03:41:46 +0000980 def GetIsGerrit(self):
981 """Return true if this repo is assosiated with gerrit code review system."""
982 if self.is_gerrit is None:
983 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
984 return self.is_gerrit
985
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000986 def GetSquashGerritUploads(self):
987 """Return true if uploads to Gerrit should be squashed by default."""
988 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700989 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
990 if self.squash_gerrit_uploads is None:
991 # Default is squash now (http://crbug.com/611892#c23).
992 self.squash_gerrit_uploads = not (
993 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
994 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000995 return self.squash_gerrit_uploads
996
tandriia60502f2016-06-20 02:01:53 -0700997 def GetSquashGerritUploadsOverride(self):
998 """Return True or False if codereview.settings should be overridden.
999
1000 Returns None if no override has been defined.
1001 """
1002 # See also http://crbug.com/611892#c23
1003 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
1004 error_ok=True).strip()
1005 if result == 'true':
1006 return True
1007 if result == 'false':
1008 return False
1009 return None
1010
tandrii@chromium.org28253532016-04-14 13:46:56 +00001011 def GetGerritSkipEnsureAuthenticated(self):
1012 """Return True if EnsureAuthenticated should not be done for Gerrit
1013 uploads."""
1014 if self.gerrit_skip_ensure_authenticated is None:
1015 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00001016 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +00001017 error_ok=True).strip() == 'true')
1018 return self.gerrit_skip_ensure_authenticated
1019
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001020 def GetGitEditor(self):
1021 """Return the editor specified in the git config, or None if none is."""
1022 if self.git_editor is None:
1023 self.git_editor = self._GetConfig('core.editor', error_ok=True)
1024 return self.git_editor or None
1025
thestig@chromium.org44202a22014-03-11 19:22:18 +00001026 def GetLintRegex(self):
1027 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
1028 DEFAULT_LINT_REGEX)
1029
1030 def GetLintIgnoreRegex(self):
1031 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
1032 DEFAULT_LINT_IGNORE_REGEX)
1033
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001034 def GetProject(self):
1035 if not self.project:
1036 self.project = self._GetRietveldConfig('project', error_ok=True)
1037 return self.project
1038
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001039 def GetForceHttpsCommitUrl(self):
1040 if not self.force_https_commit_url:
1041 self.force_https_commit_url = self._GetRietveldConfig(
1042 'force-https-commit-url', error_ok=True)
1043 return self.force_https_commit_url
1044
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001045 def GetPendingRefPrefix(self):
1046 if not self.pending_ref_prefix:
1047 self.pending_ref_prefix = self._GetRietveldConfig(
1048 'pending-ref-prefix', error_ok=True)
1049 return self.pending_ref_prefix
1050
tandriif46c20f2016-09-14 06:17:05 -07001051 def GetHasGitNumberFooter(self):
1052 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1053 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1054 if not self.git_number_footer:
1055 self.git_number_footer = self._GetRietveldConfig(
1056 'git-number-footer', error_ok=True)
1057 return self.git_number_footer
1058
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001059 def _GetRietveldConfig(self, param, **kwargs):
1060 return self._GetConfig('rietveld.' + param, **kwargs)
1061
rmistry@google.com78948ed2015-07-08 23:09:57 +00001062 def _GetBranchConfig(self, branch_name, param, **kwargs):
1063 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 def _GetConfig(self, param, **kwargs):
1066 self.LazyUpdateIfNeeded()
1067 return RunGit(['config', param], **kwargs).strip()
1068
1069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070def ShortBranchName(branch):
1071 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001072 return branch.replace('refs/heads/', '', 1)
1073
1074
1075def GetCurrentBranchRef():
1076 """Returns branch ref (e.g., refs/heads/master) or None."""
1077 return RunGit(['symbolic-ref', 'HEAD'],
1078 stderr=subprocess2.VOID, error_ok=True).strip() or None
1079
1080
1081def GetCurrentBranch():
1082 """Returns current branch or None.
1083
1084 For refs/heads/* branches, returns just last part. For others, full ref.
1085 """
1086 branchref = GetCurrentBranchRef()
1087 if branchref:
1088 return ShortBranchName(branchref)
1089 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090
1091
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001092class _CQState(object):
1093 """Enum for states of CL with respect to Commit Queue."""
1094 NONE = 'none'
1095 DRY_RUN = 'dry_run'
1096 COMMIT = 'commit'
1097
1098 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1099
1100
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001101class _ParsedIssueNumberArgument(object):
1102 def __init__(self, issue=None, patchset=None, hostname=None):
1103 self.issue = issue
1104 self.patchset = patchset
1105 self.hostname = hostname
1106
1107 @property
1108 def valid(self):
1109 return self.issue is not None
1110
1111
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001112def ParseIssueNumberArgument(arg):
1113 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1114 fail_result = _ParsedIssueNumberArgument()
1115
1116 if arg.isdigit():
1117 return _ParsedIssueNumberArgument(issue=int(arg))
1118 if not arg.startswith('http'):
1119 return fail_result
1120 url = gclient_utils.UpgradeToHttps(arg)
1121 try:
1122 parsed_url = urlparse.urlparse(url)
1123 except ValueError:
1124 return fail_result
1125 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1126 tmp = cls.ParseIssueURL(parsed_url)
1127 if tmp is not None:
1128 return tmp
1129 return fail_result
1130
1131
tandriic2405f52016-10-10 08:13:15 -07001132class GerritIssueNotExists(Exception):
1133 def __init__(self, issue, url):
1134 self.issue = issue
1135 self.url = url
1136 super(GerritIssueNotExists, self).__init__()
1137
1138 def __str__(self):
1139 return 'issue %s at %s does not exist or you have no access to it' % (
1140 self.issue, self.url)
1141
1142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 """Changelist works with one changelist in local branch.
1145
1146 Supports two codereview backends: Rietveld or Gerrit, selected at object
1147 creation.
1148
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001149 Notes:
1150 * Not safe for concurrent multi-{thread,process} use.
1151 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001152 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001153 """
1154
1155 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1156 """Create a new ChangeList instance.
1157
1158 If issue is given, the codereview must be given too.
1159
1160 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1161 Otherwise, it's decided based on current configuration of the local branch,
1162 with default being 'rietveld' for backwards compatibility.
1163 See _load_codereview_impl for more details.
1164
1165 **kwargs will be passed directly to codereview implementation.
1166 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001168 global settings
1169 if not settings:
1170 # Happens when git_cl.py is used as a utility library.
1171 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172
1173 if issue:
1174 assert codereview, 'codereview must be known, if issue is known'
1175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 self.branchref = branchref
1177 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001178 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 self.branch = ShortBranchName(self.branchref)
1180 else:
1181 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001183 self.lookedup_issue = False
1184 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 self.has_description = False
1186 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001187 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001189 self.cc = None
1190 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001191 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001192
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001193 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001194 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001196 assert self._codereview_impl
1197 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001198
1199 def _load_codereview_impl(self, codereview=None, **kwargs):
1200 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001201 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1202 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1203 self._codereview = codereview
1204 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001205 return
1206
1207 # Automatic selection based on issue number set for a current branch.
1208 # Rietveld takes precedence over Gerrit.
1209 assert not self.issue
1210 # Whether we find issue or not, we are doing the lookup.
1211 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001212 if self.GetBranch():
1213 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1214 issue = _git_get_branch_config_value(
1215 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1216 if issue:
1217 self._codereview = codereview
1218 self._codereview_impl = cls(self, **kwargs)
1219 self.issue = int(issue)
1220 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001221
1222 # No issue is set for this branch, so decide based on repo-wide settings.
1223 return self._load_codereview_impl(
1224 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1225 **kwargs)
1226
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001227 def IsGerrit(self):
1228 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001229
1230 def GetCCList(self):
1231 """Return the users cc'd on this CL.
1232
agable92bec4f2016-08-24 09:27:27 -07001233 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001234 """
1235 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001236 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001237 more_cc = ','.join(self.watchers)
1238 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1239 return self.cc
1240
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001241 def GetCCListWithoutDefault(self):
1242 """Return the users cc'd on this CL excluding default ones."""
1243 if self.cc is None:
1244 self.cc = ','.join(self.watchers)
1245 return self.cc
1246
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001247 def SetWatchers(self, watchers):
1248 """Set the list of email addresses that should be cc'd based on the changed
1249 files in this CL.
1250 """
1251 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252
1253 def GetBranch(self):
1254 """Returns the short branch name, e.g. 'master'."""
1255 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001257 if not branchref:
1258 return None
1259 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 self.branch = ShortBranchName(self.branchref)
1261 return self.branch
1262
1263 def GetBranchRef(self):
1264 """Returns the full branch name, e.g. 'refs/heads/master'."""
1265 self.GetBranch() # Poke the lazy loader.
1266 return self.branchref
1267
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001268 def ClearBranch(self):
1269 """Clears cached branch data of this object."""
1270 self.branch = self.branchref = None
1271
tandrii5d48c322016-08-18 16:19:37 -07001272 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1273 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1274 kwargs['branch'] = self.GetBranch()
1275 return _git_get_branch_config_value(key, default, **kwargs)
1276
1277 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1278 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1279 assert self.GetBranch(), (
1280 'this CL must have an associated branch to %sset %s%s' %
1281 ('un' if value is None else '',
1282 key,
1283 '' if value is None else ' to %r' % value))
1284 kwargs['branch'] = self.GetBranch()
1285 return _git_set_branch_config_value(key, value, **kwargs)
1286
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 @staticmethod
1288 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001289 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 e.g. 'origin', 'refs/heads/master'
1291 """
1292 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001293 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001296 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001298 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1299 error_ok=True).strip()
1300 if upstream_branch:
1301 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001303 # Fall back on trying a git-svn upstream branch.
1304 if settings.GetIsGitSvn():
1305 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001307 # Else, try to guess the origin remote.
1308 remote_branches = RunGit(['branch', '-r']).split()
1309 if 'origin/master' in remote_branches:
1310 # Fall back on origin/master if it exits.
1311 remote = 'origin'
1312 upstream_branch = 'refs/heads/master'
1313 elif 'origin/trunk' in remote_branches:
1314 # Fall back on origin/trunk if it exists. Generally a shared
1315 # git-svn clone
1316 remote = 'origin'
1317 upstream_branch = 'refs/heads/trunk'
1318 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001319 DieWithError(
1320 'Unable to determine default branch to diff against.\n'
1321 'Either pass complete "git diff"-style arguments, like\n'
1322 ' git cl upload origin/master\n'
1323 'or verify this branch is set up to track another \n'
1324 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325
1326 return remote, upstream_branch
1327
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001328 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001329 upstream_branch = self.GetUpstreamBranch()
1330 if not BranchExists(upstream_branch):
1331 DieWithError('The upstream for the current branch (%s) does not exist '
1332 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001333 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001334 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001335
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 def GetUpstreamBranch(self):
1337 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001338 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001340 upstream_branch = upstream_branch.replace('refs/heads/',
1341 'refs/remotes/%s/' % remote)
1342 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1343 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 self.upstream_branch = upstream_branch
1345 return self.upstream_branch
1346
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001347 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001348 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001349 remote, branch = None, self.GetBranch()
1350 seen_branches = set()
1351 while branch not in seen_branches:
1352 seen_branches.add(branch)
1353 remote, branch = self.FetchUpstreamTuple(branch)
1354 branch = ShortBranchName(branch)
1355 if remote != '.' or branch.startswith('refs/remotes'):
1356 break
1357 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001358 remotes = RunGit(['remote'], error_ok=True).split()
1359 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001360 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001361 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001362 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001363 logging.warning('Could not determine which remote this change is '
1364 'associated with, so defaulting to "%s". This may '
1365 'not be what you want. You may prevent this message '
1366 'by running "git svn info" as documented here: %s',
1367 self._remote,
1368 GIT_INSTRUCTIONS_URL)
1369 else:
1370 logging.warn('Could not determine which remote this change is '
1371 'associated with. You may prevent this message by '
1372 'running "git svn info" as documented here: %s',
1373 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001374 branch = 'HEAD'
1375 if branch.startswith('refs/remotes'):
1376 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001377 elif branch.startswith('refs/branch-heads/'):
1378 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001379 else:
1380 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001381 return self._remote
1382
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001383 def GitSanityChecks(self, upstream_git_obj):
1384 """Checks git repo status and ensures diff is from local commits."""
1385
sbc@chromium.org79706062015-01-14 21:18:12 +00001386 if upstream_git_obj is None:
1387 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001388 print('ERROR: unable to determine current branch (detached HEAD?)',
1389 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001390 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001391 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001392 return False
1393
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001394 # Verify the commit we're diffing against is in our current branch.
1395 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1396 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1397 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001398 print('ERROR: %s is not in the current branch. You may need to rebase '
1399 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001400 return False
1401
1402 # List the commits inside the diff, and verify they are all local.
1403 commits_in_diff = RunGit(
1404 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1405 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1406 remote_branch = remote_branch.strip()
1407 if code != 0:
1408 _, remote_branch = self.GetRemoteBranch()
1409
1410 commits_in_remote = RunGit(
1411 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1412
1413 common_commits = set(commits_in_diff) & set(commits_in_remote)
1414 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001415 print('ERROR: Your diff contains %d commits already in %s.\n'
1416 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1417 'the diff. If you are using a custom git flow, you can override'
1418 ' the reference used for this check with "git config '
1419 'gitcl.remotebranch <git-ref>".' % (
1420 len(common_commits), remote_branch, upstream_git_obj),
1421 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001422 return False
1423 return True
1424
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001425 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001426 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001427
1428 Returns None if it is not set.
1429 """
tandrii5d48c322016-08-18 16:19:37 -07001430 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001431
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001432 def GetGitSvnRemoteUrl(self):
1433 """Return the configured git-svn remote URL parsed from git svn info.
1434
1435 Returns None if it is not set.
1436 """
1437 # URL is dependent on the current directory.
1438 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1439 if data:
1440 keys = dict(line.split(': ', 1) for line in data.splitlines()
1441 if ': ' in line)
1442 return keys.get('URL', None)
1443 return None
1444
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445 def GetRemoteUrl(self):
1446 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1447
1448 Returns None if there is no remote.
1449 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001450 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001451 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1452
1453 # If URL is pointing to a local directory, it is probably a git cache.
1454 if os.path.isdir(url):
1455 url = RunGit(['config', 'remote.%s.url' % remote],
1456 error_ok=True,
1457 cwd=url).strip()
1458 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001460 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001461 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001462 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001463 self.issue = self._GitGetBranchConfigValue(
1464 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001465 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 return self.issue
1467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 def GetIssueURL(self):
1469 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001470 issue = self.GetIssue()
1471 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001472 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001473 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
1475 def GetDescription(self, pretty=False):
1476 if not self.has_description:
1477 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001478 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 self.has_description = True
1480 if pretty:
1481 wrapper = textwrap.TextWrapper()
1482 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1483 return wrapper.fill(self.description)
1484 return self.description
1485
1486 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001487 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001488 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001489 self.patchset = self._GitGetBranchConfigValue(
1490 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001491 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 return self.patchset
1493
1494 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001495 """Set this branch's patchset. If patchset=0, clears the patchset."""
1496 assert self.GetBranch()
1497 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001498 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001499 else:
1500 self.patchset = int(patchset)
1501 self._GitSetBranchConfigValue(
1502 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001504 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001505 """Set this branch's issue. If issue isn't given, clears the issue."""
1506 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001508 issue = int(issue)
1509 self._GitSetBranchConfigValue(
1510 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001511 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001512 codereview_server = self._codereview_impl.GetCodereviewServer()
1513 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001514 self._GitSetBranchConfigValue(
1515 self._codereview_impl.CodereviewServerConfigKey(),
1516 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517 else:
tandrii5d48c322016-08-18 16:19:37 -07001518 # Reset all of these just to be clean.
1519 reset_suffixes = [
1520 'last-upload-hash',
1521 self._codereview_impl.IssueConfigKey(),
1522 self._codereview_impl.PatchsetConfigKey(),
1523 self._codereview_impl.CodereviewServerConfigKey(),
1524 ] + self._PostUnsetIssueProperties()
1525 for prop in reset_suffixes:
1526 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001527 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001528 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529
dnjba1b0f32016-09-02 12:37:42 -07001530 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001531 if not self.GitSanityChecks(upstream_branch):
1532 DieWithError('\nGit sanity check failure')
1533
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001534 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001535 if not root:
1536 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001537 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001538
1539 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001540 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001541 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001542 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001543 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001544 except subprocess2.CalledProcessError:
1545 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001546 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001547 'This branch probably doesn\'t exist anymore. To reset the\n'
1548 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001549 ' git branch --set-upstream-to origin/master %s\n'
1550 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001551 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001552
maruel@chromium.org52424302012-08-29 15:14:30 +00001553 issue = self.GetIssue()
1554 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001555 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001556 description = self.GetDescription()
1557 else:
1558 # If the change was never uploaded, use the log messages of all commits
1559 # up to the branch point, as git cl upload will prefill the description
1560 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001561 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1562 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001563
1564 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001565 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001566 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001567 name,
1568 description,
1569 absroot,
1570 files,
1571 issue,
1572 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001573 author,
1574 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001575
dsansomee2d6fd92016-09-08 00:10:47 -07001576 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001577 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001578 return self._codereview_impl.UpdateDescriptionRemote(
1579 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001580
1581 def RunHook(self, committing, may_prompt, verbose, change):
1582 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1583 try:
1584 return presubmit_support.DoPresubmitChecks(change, committing,
1585 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1586 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001587 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1588 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001589 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 DieWithError(
1591 ('%s\nMaybe your depot_tools is out of date?\n'
1592 'If all fails, contact maruel@') % e)
1593
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001594 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1595 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001596 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1597 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001598 else:
1599 # Assume url.
1600 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1601 urlparse.urlparse(issue_arg))
1602 if not parsed_issue_arg or not parsed_issue_arg.valid:
1603 DieWithError('Failed to parse issue argument "%s". '
1604 'Must be an issue number or a valid URL.' % issue_arg)
1605 return self._codereview_impl.CMDPatchWithParsedIssue(
1606 parsed_issue_arg, reject, nocommit, directory)
1607
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001608 def CMDUpload(self, options, git_diff_args, orig_args):
1609 """Uploads a change to codereview."""
1610 if git_diff_args:
1611 # TODO(ukai): is it ok for gerrit case?
1612 base_branch = git_diff_args[0]
1613 else:
1614 if self.GetBranch() is None:
1615 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1616
1617 # Default to diffing against common ancestor of upstream branch
1618 base_branch = self.GetCommonAncestorWithUpstream()
1619 git_diff_args = [base_branch, 'HEAD']
1620
1621 # Make sure authenticated to codereview before running potentially expensive
1622 # hooks. It is a fast, best efforts check. Codereview still can reject the
1623 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001624 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625
1626 # Apply watchlists on upload.
1627 change = self.GetChange(base_branch, None)
1628 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1629 files = [f.LocalPath() for f in change.AffectedFiles()]
1630 if not options.bypass_watchlists:
1631 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1632
1633 if not options.bypass_hooks:
1634 if options.reviewers or options.tbr_owners:
1635 # Set the reviewer list now so that presubmit checks can access it.
1636 change_description = ChangeDescription(change.FullDescriptionText())
1637 change_description.update_reviewers(options.reviewers,
1638 options.tbr_owners,
1639 change)
1640 change.SetDescriptionText(change_description.description)
1641 hook_results = self.RunHook(committing=False,
1642 may_prompt=not options.force,
1643 verbose=options.verbose,
1644 change=change)
1645 if not hook_results.should_continue():
1646 return 1
1647 if not options.reviewers and hook_results.reviewers:
1648 options.reviewers = hook_results.reviewers.split(',')
1649
1650 if self.GetIssue():
1651 latest_patchset = self.GetMostRecentPatchset()
1652 local_patchset = self.GetPatchset()
1653 if (latest_patchset and local_patchset and
1654 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001655 print('The last upload made from this repository was patchset #%d but '
1656 'the most recent patchset on the server is #%d.'
1657 % (local_patchset, latest_patchset))
1658 print('Uploading will still work, but if you\'ve uploaded to this '
1659 'issue from another machine or branch the patch you\'re '
1660 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001661 ask_for_data('About to upload; enter to confirm.')
1662
1663 print_stats(options.similarity, options.find_copies, git_diff_args)
1664 ret = self.CMDUploadChange(options, git_diff_args, change)
1665 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001666 if options.use_commit_queue:
1667 self.SetCQState(_CQState.COMMIT)
1668 elif options.cq_dry_run:
1669 self.SetCQState(_CQState.DRY_RUN)
1670
tandrii5d48c322016-08-18 16:19:37 -07001671 _git_set_branch_config_value('last-upload-hash',
1672 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001673 # Run post upload hooks, if specified.
1674 if settings.GetRunPostUploadHook():
1675 presubmit_support.DoPostUploadExecuter(
1676 change,
1677 self,
1678 settings.GetRoot(),
1679 options.verbose,
1680 sys.stdout)
1681
1682 # Upload all dependencies if specified.
1683 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001684 print()
1685 print('--dependencies has been specified.')
1686 print('All dependent local branches will be re-uploaded.')
1687 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001688 # Remove the dependencies flag from args so that we do not end up in a
1689 # loop.
1690 orig_args.remove('--dependencies')
1691 ret = upload_branch_deps(self, orig_args)
1692 return ret
1693
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001694 def SetCQState(self, new_state):
1695 """Update the CQ state for latest patchset.
1696
1697 Issue must have been already uploaded and known.
1698 """
1699 assert new_state in _CQState.ALL_STATES
1700 assert self.GetIssue()
1701 return self._codereview_impl.SetCQState(new_state)
1702
qyearsley1fdfcb62016-10-24 13:22:03 -07001703 def TriggerDryRun(self):
1704 """Triggers a dry run and prints a warning on failure."""
1705 # TODO(qyearsley): Either re-use this method in CMDset_commit
1706 # and CMDupload, or change CMDtry to trigger dry runs with
1707 # just SetCQState, and catch keyboard interrupt and other
1708 # errors in that method.
1709 try:
1710 self.SetCQState(_CQState.DRY_RUN)
1711 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1712 return 0
1713 except KeyboardInterrupt:
1714 raise
1715 except:
1716 print('WARNING: failed to trigger CQ Dry Run.\n'
1717 'Either:\n'
1718 ' * your project has no CQ\n'
1719 ' * you don\'t have permission to trigger Dry Run\n'
1720 ' * bug in this code (see stack trace below).\n'
1721 'Consider specifying which bots to trigger manually '
1722 'or asking your project owners for permissions '
1723 'or contacting Chrome Infrastructure team at '
1724 'https://www.chromium.org/infra\n\n')
1725 # Still raise exception so that stack trace is printed.
1726 raise
1727
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001728 # Forward methods to codereview specific implementation.
1729
1730 def CloseIssue(self):
1731 return self._codereview_impl.CloseIssue()
1732
1733 def GetStatus(self):
1734 return self._codereview_impl.GetStatus()
1735
1736 def GetCodereviewServer(self):
1737 return self._codereview_impl.GetCodereviewServer()
1738
tandriide281ae2016-10-12 06:02:30 -07001739 def GetIssueOwner(self):
1740 """Get owner from codereview, which may differ from this checkout."""
1741 return self._codereview_impl.GetIssueOwner()
1742
1743 def GetIssueProject(self):
1744 """Get project from codereview, which may differ from what this
1745 checkout's codereview.settings or gerrit project URL say.
1746 """
1747 return self._codereview_impl.GetIssueProject()
1748
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001749 def GetApprovingReviewers(self):
1750 return self._codereview_impl.GetApprovingReviewers()
1751
1752 def GetMostRecentPatchset(self):
1753 return self._codereview_impl.GetMostRecentPatchset()
1754
tandriide281ae2016-10-12 06:02:30 -07001755 def CannotTriggerTryJobReason(self):
1756 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1757 return self._codereview_impl.CannotTriggerTryJobReason()
1758
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 def __getattr__(self, attr):
1760 # This is because lots of untested code accesses Rietveld-specific stuff
1761 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001762 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001763 # Note that child method defines __getattr__ as well, and forwards it here,
1764 # because _RietveldChangelistImpl is not cleaned up yet, and given
1765 # deprecation of Rietveld, it should probably be just removed.
1766 # Until that time, avoid infinite recursion by bypassing __getattr__
1767 # of implementation class.
1768 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769
1770
1771class _ChangelistCodereviewBase(object):
1772 """Abstract base class encapsulating codereview specifics of a changelist."""
1773 def __init__(self, changelist):
1774 self._changelist = changelist # instance of Changelist
1775
1776 def __getattr__(self, attr):
1777 # Forward methods to changelist.
1778 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1779 # _RietveldChangelistImpl to avoid this hack?
1780 return getattr(self._changelist, attr)
1781
1782 def GetStatus(self):
1783 """Apply a rough heuristic to give a simple summary of an issue's review
1784 or CQ status, assuming adherence to a common workflow.
1785
1786 Returns None if no issue for this branch, or specific string keywords.
1787 """
1788 raise NotImplementedError()
1789
1790 def GetCodereviewServer(self):
1791 """Returns server URL without end slash, like "https://codereview.com"."""
1792 raise NotImplementedError()
1793
1794 def FetchDescription(self):
1795 """Fetches and returns description from the codereview server."""
1796 raise NotImplementedError()
1797
tandrii5d48c322016-08-18 16:19:37 -07001798 @classmethod
1799 def IssueConfigKey(cls):
1800 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 raise NotImplementedError()
1802
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001803 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001804 def PatchsetConfigKey(cls):
1805 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001806 raise NotImplementedError()
1807
tandrii5d48c322016-08-18 16:19:37 -07001808 @classmethod
1809 def CodereviewServerConfigKey(cls):
1810 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 raise NotImplementedError()
1812
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001813 def _PostUnsetIssueProperties(self):
1814 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001815 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001816
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001817 def GetRieveldObjForPresubmit(self):
1818 # This is an unfortunate Rietveld-embeddedness in presubmit.
1819 # For non-Rietveld codereviews, this probably should return a dummy object.
1820 raise NotImplementedError()
1821
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001822 def GetGerritObjForPresubmit(self):
1823 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1824 return None
1825
dsansomee2d6fd92016-09-08 00:10:47 -07001826 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 """Update the description on codereview site."""
1828 raise NotImplementedError()
1829
1830 def CloseIssue(self):
1831 """Closes the issue."""
1832 raise NotImplementedError()
1833
1834 def GetApprovingReviewers(self):
1835 """Returns a list of reviewers approving the change.
1836
1837 Note: not necessarily committers.
1838 """
1839 raise NotImplementedError()
1840
1841 def GetMostRecentPatchset(self):
1842 """Returns the most recent patchset number from the codereview site."""
1843 raise NotImplementedError()
1844
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001845 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1846 directory):
1847 """Fetches and applies the issue.
1848
1849 Arguments:
1850 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1851 reject: if True, reject the failed patch instead of switching to 3-way
1852 merge. Rietveld only.
1853 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1854 only.
1855 directory: switch to directory before applying the patch. Rietveld only.
1856 """
1857 raise NotImplementedError()
1858
1859 @staticmethod
1860 def ParseIssueURL(parsed_url):
1861 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1862 failed."""
1863 raise NotImplementedError()
1864
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001865 def EnsureAuthenticated(self, force):
1866 """Best effort check that user is authenticated with codereview server.
1867
1868 Arguments:
1869 force: whether to skip confirmation questions.
1870 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001871 raise NotImplementedError()
1872
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001873 def CMDUploadChange(self, options, args, change):
1874 """Uploads a change to codereview."""
1875 raise NotImplementedError()
1876
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001877 def SetCQState(self, new_state):
1878 """Update the CQ state for latest patchset.
1879
1880 Issue must have been already uploaded and known.
1881 """
1882 raise NotImplementedError()
1883
tandriie113dfd2016-10-11 10:20:12 -07001884 def CannotTriggerTryJobReason(self):
1885 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1886 raise NotImplementedError()
1887
tandriide281ae2016-10-12 06:02:30 -07001888 def GetIssueOwner(self):
1889 raise NotImplementedError()
1890
1891 def GetIssueProject(self):
1892 raise NotImplementedError()
1893
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001894
1895class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1896 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1897 super(_RietveldChangelistImpl, self).__init__(changelist)
1898 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001899 if not rietveld_server:
1900 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001901
1902 self._rietveld_server = rietveld_server
1903 self._auth_config = auth_config
1904 self._props = None
1905 self._rpc_server = None
1906
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001907 def GetCodereviewServer(self):
1908 if not self._rietveld_server:
1909 # If we're on a branch then get the server potentially associated
1910 # with that branch.
1911 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001912 self._rietveld_server = gclient_utils.UpgradeToHttps(
1913 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914 if not self._rietveld_server:
1915 self._rietveld_server = settings.GetDefaultServerUrl()
1916 return self._rietveld_server
1917
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001918 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001919 """Best effort check that user is authenticated with Rietveld server."""
1920 if self._auth_config.use_oauth2:
1921 authenticator = auth.get_authenticator_for_host(
1922 self.GetCodereviewServer(), self._auth_config)
1923 if not authenticator.has_cached_credentials():
1924 raise auth.LoginRequiredError(self.GetCodereviewServer())
1925
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 def FetchDescription(self):
1927 issue = self.GetIssue()
1928 assert issue
1929 try:
1930 return self.RpcServer().get_description(issue).strip()
1931 except urllib2.HTTPError as e:
1932 if e.code == 404:
1933 DieWithError(
1934 ('\nWhile fetching the description for issue %d, received a '
1935 '404 (not found)\n'
1936 'error. It is likely that you deleted this '
1937 'issue on the server. If this is the\n'
1938 'case, please run\n\n'
1939 ' git cl issue 0\n\n'
1940 'to clear the association with the deleted issue. Then run '
1941 'this command again.') % issue)
1942 else:
1943 DieWithError(
1944 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1945 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001946 print('Warning: Failed to retrieve CL description due to network '
1947 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001948 return ''
1949
1950 def GetMostRecentPatchset(self):
1951 return self.GetIssueProperties()['patchsets'][-1]
1952
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001953 def GetIssueProperties(self):
1954 if self._props is None:
1955 issue = self.GetIssue()
1956 if not issue:
1957 self._props = {}
1958 else:
1959 self._props = self.RpcServer().get_issue_properties(issue, True)
1960 return self._props
1961
tandriie113dfd2016-10-11 10:20:12 -07001962 def CannotTriggerTryJobReason(self):
1963 props = self.GetIssueProperties()
1964 if not props:
1965 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1966 if props.get('closed'):
1967 return 'CL %s is closed' % self.GetIssue()
1968 if props.get('private'):
1969 return 'CL %s is private' % self.GetIssue()
1970 return None
1971
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001972 def GetApprovingReviewers(self):
1973 return get_approving_reviewers(self.GetIssueProperties())
1974
tandriide281ae2016-10-12 06:02:30 -07001975 def GetIssueOwner(self):
1976 return (self.GetIssueProperties() or {}).get('owner_email')
1977
1978 def GetIssueProject(self):
1979 return (self.GetIssueProperties() or {}).get('project')
1980
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001981 def AddComment(self, message):
1982 return self.RpcServer().add_comment(self.GetIssue(), message)
1983
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001984 def GetStatus(self):
1985 """Apply a rough heuristic to give a simple summary of an issue's review
1986 or CQ status, assuming adherence to a common workflow.
1987
1988 Returns None if no issue for this branch, or one of the following keywords:
1989 * 'error' - error from review tool (including deleted issues)
1990 * 'unsent' - not sent for review
1991 * 'waiting' - waiting for review
1992 * 'reply' - waiting for owner to reply to review
1993 * 'lgtm' - LGTM from at least one approved reviewer
1994 * 'commit' - in the commit queue
1995 * 'closed' - closed
1996 """
1997 if not self.GetIssue():
1998 return None
1999
2000 try:
2001 props = self.GetIssueProperties()
2002 except urllib2.HTTPError:
2003 return 'error'
2004
2005 if props.get('closed'):
2006 # Issue is closed.
2007 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002008 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002009 # Issue is in the commit queue.
2010 return 'commit'
2011
2012 try:
2013 reviewers = self.GetApprovingReviewers()
2014 except urllib2.HTTPError:
2015 return 'error'
2016
2017 if reviewers:
2018 # Was LGTM'ed.
2019 return 'lgtm'
2020
2021 messages = props.get('messages') or []
2022
tandrii9d2c7a32016-06-22 03:42:45 -07002023 # Skip CQ messages that don't require owner's action.
2024 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2025 if 'Dry run:' in messages[-1]['text']:
2026 messages.pop()
2027 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2028 # This message always follows prior messages from CQ,
2029 # so skip this too.
2030 messages.pop()
2031 else:
2032 # This is probably a CQ messages warranting user attention.
2033 break
2034
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002035 if not messages:
2036 # No message was sent.
2037 return 'unsent'
2038 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002039 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002040 return 'reply'
2041 return 'waiting'
2042
dsansomee2d6fd92016-09-08 00:10:47 -07002043 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002044 return self.RpcServer().update_description(
2045 self.GetIssue(), self.description)
2046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002048 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002050 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002051 return self.SetFlags({flag: value})
2052
2053 def SetFlags(self, flags):
2054 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002055 """
phajdan.jr68598232016-08-10 03:28:28 -07002056 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002057 try:
tandrii4b233bd2016-07-06 03:50:29 -07002058 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002059 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002060 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002061 if e.code == 404:
2062 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2063 if e.code == 403:
2064 DieWithError(
2065 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002066 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002067 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002069 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 """Returns an upload.RpcServer() to access this review's rietveld instance.
2071 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002072 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002073 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002074 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002075 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002076 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002078 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002079 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002080 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081
tandrii5d48c322016-08-18 16:19:37 -07002082 @classmethod
2083 def PatchsetConfigKey(cls):
2084 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002085
tandrii5d48c322016-08-18 16:19:37 -07002086 @classmethod
2087 def CodereviewServerConfigKey(cls):
2088 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002089
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002090 def GetRieveldObjForPresubmit(self):
2091 return self.RpcServer()
2092
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002093 def SetCQState(self, new_state):
2094 props = self.GetIssueProperties()
2095 if props.get('private'):
2096 DieWithError('Cannot set-commit on private issue')
2097
2098 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002099 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002100 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002101 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002102 else:
tandrii4b233bd2016-07-06 03:50:29 -07002103 assert new_state == _CQState.DRY_RUN
2104 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002105
2106
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2108 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002109 # PatchIssue should never be called with a dirty tree. It is up to the
2110 # caller to check this, but just in case we assert here since the
2111 # consequences of the caller not checking this could be dire.
2112 assert(not git_common.is_dirty_git_tree('apply'))
2113 assert(parsed_issue_arg.valid)
2114 self._changelist.issue = parsed_issue_arg.issue
2115 if parsed_issue_arg.hostname:
2116 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2117
skobes6468b902016-10-24 08:45:10 -07002118 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2119 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2120 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002121 try:
skobes6468b902016-10-24 08:45:10 -07002122 scm_obj.apply_patch(patchset_object)
2123 except Exception as e:
2124 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 return 1
2126
2127 # If we had an issue, commit the current state and register the issue.
2128 if not nocommit:
2129 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2130 'patch from issue %(i)s at patchset '
2131 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2132 % {'i': self.GetIssue(), 'p': patchset})])
2133 self.SetIssue(self.GetIssue())
2134 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002135 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002136 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002137 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002138 return 0
2139
2140 @staticmethod
2141 def ParseIssueURL(parsed_url):
2142 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2143 return None
wychen3c1c1722016-08-04 11:46:36 -07002144 # Rietveld patch: https://domain/<number>/#ps<patchset>
2145 match = re.match(r'/(\d+)/$', parsed_url.path)
2146 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2147 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002148 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002149 issue=int(match.group(1)),
2150 patchset=int(match2.group(1)),
2151 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002152 # Typical url: https://domain/<issue_number>[/[other]]
2153 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2154 if match:
skobes6468b902016-10-24 08:45:10 -07002155 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 issue=int(match.group(1)),
2157 hostname=parsed_url.netloc)
2158 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2159 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2160 if match:
skobes6468b902016-10-24 08:45:10 -07002161 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002162 issue=int(match.group(1)),
2163 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002164 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 return None
2166
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002167 def CMDUploadChange(self, options, args, change):
2168 """Upload the patch to Rietveld."""
2169 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2170 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002171 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2172 if options.emulate_svn_auto_props:
2173 upload_args.append('--emulate_svn_auto_props')
2174
2175 change_desc = None
2176
2177 if options.email is not None:
2178 upload_args.extend(['--email', options.email])
2179
2180 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002181 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002182 upload_args.extend(['--title', options.title])
2183 if options.message:
2184 upload_args.extend(['--message', options.message])
2185 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002186 print('This branch is associated with issue %s. '
2187 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002188 else:
nodirca166002016-06-27 10:59:51 -07002189 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002190 upload_args.extend(['--title', options.title])
2191 message = (options.title or options.message or
2192 CreateDescriptionFromLog(args))
2193 change_desc = ChangeDescription(message)
2194 if options.reviewers or options.tbr_owners:
2195 change_desc.update_reviewers(options.reviewers,
2196 options.tbr_owners,
2197 change)
2198 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002199 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200
2201 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002202 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002203 return 1
2204
2205 upload_args.extend(['--message', change_desc.description])
2206 if change_desc.get_reviewers():
2207 upload_args.append('--reviewers=%s' % ','.join(
2208 change_desc.get_reviewers()))
2209 if options.send_mail:
2210 if not change_desc.get_reviewers():
2211 DieWithError("Must specify reviewers to send email.")
2212 upload_args.append('--send_mail')
2213
2214 # We check this before applying rietveld.private assuming that in
2215 # rietveld.cc only addresses which we can send private CLs to are listed
2216 # if rietveld.private is set, and so we should ignore rietveld.cc only
2217 # when --private is specified explicitly on the command line.
2218 if options.private:
2219 logging.warn('rietveld.cc is ignored since private flag is specified. '
2220 'You need to review and add them manually if necessary.')
2221 cc = self.GetCCListWithoutDefault()
2222 else:
2223 cc = self.GetCCList()
2224 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002225 if change_desc.get_cced():
2226 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 if cc:
2228 upload_args.extend(['--cc', cc])
2229
2230 if options.private or settings.GetDefaultPrivateFlag() == "True":
2231 upload_args.append('--private')
2232
2233 upload_args.extend(['--git_similarity', str(options.similarity)])
2234 if not options.find_copies:
2235 upload_args.extend(['--git_no_find_copies'])
2236
2237 # Include the upstream repo's URL in the change -- this is useful for
2238 # projects that have their source spread across multiple repos.
2239 remote_url = self.GetGitBaseUrlFromConfig()
2240 if not remote_url:
2241 if settings.GetIsGitSvn():
2242 remote_url = self.GetGitSvnRemoteUrl()
2243 else:
2244 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2245 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2246 self.GetUpstreamBranch().split('/')[-1])
2247 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 remote, remote_branch = self.GetRemoteBranch()
2249 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2250 settings.GetPendingRefPrefix())
2251 if target_ref:
2252 upload_args.extend(['--target_ref', target_ref])
2253
2254 # Look for dependent patchsets. See crbug.com/480453 for more details.
2255 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2256 upstream_branch = ShortBranchName(upstream_branch)
2257 if remote is '.':
2258 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002259 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002261 print()
2262 print('Skipping dependency patchset upload because git config '
2263 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2264 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002265 else:
2266 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002267 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 auth_config=auth_config)
2269 branch_cl_issue_url = branch_cl.GetIssueURL()
2270 branch_cl_issue = branch_cl.GetIssue()
2271 branch_cl_patchset = branch_cl.GetPatchset()
2272 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2273 upload_args.extend(
2274 ['--depends_on_patchset', '%s:%s' % (
2275 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002276 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002277 '\n'
2278 'The current branch (%s) is tracking a local branch (%s) with '
2279 'an associated CL.\n'
2280 'Adding %s/#ps%s as a dependency patchset.\n'
2281 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2282 branch_cl_patchset))
2283
2284 project = settings.GetProject()
2285 if project:
2286 upload_args.extend(['--project', project])
2287
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002288 try:
2289 upload_args = ['upload'] + upload_args + args
2290 logging.info('upload.RealMain(%s)', upload_args)
2291 issue, patchset = upload.RealMain(upload_args)
2292 issue = int(issue)
2293 patchset = int(patchset)
2294 except KeyboardInterrupt:
2295 sys.exit(1)
2296 except:
2297 # If we got an exception after the user typed a description for their
2298 # change, back up the description before re-raising.
2299 if change_desc:
2300 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2301 print('\nGot exception while uploading -- saving description to %s\n' %
2302 backup_path)
2303 backup_file = open(backup_path, 'w')
2304 backup_file.write(change_desc.description)
2305 backup_file.close()
2306 raise
2307
2308 if not self.GetIssue():
2309 self.SetIssue(issue)
2310 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 return 0
2312
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002313
2314class _GerritChangelistImpl(_ChangelistCodereviewBase):
2315 def __init__(self, changelist, auth_config=None):
2316 # auth_config is Rietveld thing, kept here to preserve interface only.
2317 super(_GerritChangelistImpl, self).__init__(changelist)
2318 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002319 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002320 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002321 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002322
2323 def _GetGerritHost(self):
2324 # Lazy load of configs.
2325 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002326 if self._gerrit_host and '.' not in self._gerrit_host:
2327 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2328 # This happens for internal stuff http://crbug.com/614312.
2329 parsed = urlparse.urlparse(self.GetRemoteUrl())
2330 if parsed.scheme == 'sso':
2331 print('WARNING: using non https URLs for remote is likely broken\n'
2332 ' Your current remote is: %s' % self.GetRemoteUrl())
2333 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2334 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002335 return self._gerrit_host
2336
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002337 def _GetGitHost(self):
2338 """Returns git host to be used when uploading change to Gerrit."""
2339 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2340
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002341 def GetCodereviewServer(self):
2342 if not self._gerrit_server:
2343 # If we're on a branch then get the server potentially associated
2344 # with that branch.
2345 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002346 self._gerrit_server = self._GitGetBranchConfigValue(
2347 self.CodereviewServerConfigKey())
2348 if self._gerrit_server:
2349 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002350 if not self._gerrit_server:
2351 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2352 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002353 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002354 parts[0] = parts[0] + '-review'
2355 self._gerrit_host = '.'.join(parts)
2356 self._gerrit_server = 'https://%s' % self._gerrit_host
2357 return self._gerrit_server
2358
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002359 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002360 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002361 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002362
tandrii5d48c322016-08-18 16:19:37 -07002363 @classmethod
2364 def PatchsetConfigKey(cls):
2365 return 'gerritpatchset'
2366
2367 @classmethod
2368 def CodereviewServerConfigKey(cls):
2369 return 'gerritserver'
2370
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002371 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002372 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002373 if settings.GetGerritSkipEnsureAuthenticated():
2374 # For projects with unusual authentication schemes.
2375 # See http://crbug.com/603378.
2376 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002377 # Lazy-loader to identify Gerrit and Git hosts.
2378 if gerrit_util.GceAuthenticator.is_gce():
2379 return
2380 self.GetCodereviewServer()
2381 git_host = self._GetGitHost()
2382 assert self._gerrit_server and self._gerrit_host
2383 cookie_auth = gerrit_util.CookiesAuthenticator()
2384
2385 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2386 git_auth = cookie_auth.get_auth_header(git_host)
2387 if gerrit_auth and git_auth:
2388 if gerrit_auth == git_auth:
2389 return
2390 print((
2391 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2392 ' Check your %s or %s file for credentials of hosts:\n'
2393 ' %s\n'
2394 ' %s\n'
2395 ' %s') %
2396 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2397 git_host, self._gerrit_host,
2398 cookie_auth.get_new_password_message(git_host)))
2399 if not force:
2400 ask_for_data('If you know what you are doing, press Enter to continue, '
2401 'Ctrl+C to abort.')
2402 return
2403 else:
2404 missing = (
2405 [] if gerrit_auth else [self._gerrit_host] +
2406 [] if git_auth else [git_host])
2407 DieWithError('Credentials for the following hosts are required:\n'
2408 ' %s\n'
2409 'These are read from %s (or legacy %s)\n'
2410 '%s' % (
2411 '\n '.join(missing),
2412 cookie_auth.get_gitcookies_path(),
2413 cookie_auth.get_netrc_path(),
2414 cookie_auth.get_new_password_message(git_host)))
2415
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002416 def _PostUnsetIssueProperties(self):
2417 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002418 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002419
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002420 def GetRieveldObjForPresubmit(self):
2421 class ThisIsNotRietveldIssue(object):
2422 def __nonzero__(self):
2423 # This is a hack to make presubmit_support think that rietveld is not
2424 # defined, yet still ensure that calls directly result in a decent
2425 # exception message below.
2426 return False
2427
2428 def __getattr__(self, attr):
2429 print(
2430 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2431 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2432 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2433 'or use Rietveld for codereview.\n'
2434 'See also http://crbug.com/579160.' % attr)
2435 raise NotImplementedError()
2436 return ThisIsNotRietveldIssue()
2437
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002438 def GetGerritObjForPresubmit(self):
2439 return presubmit_support.GerritAccessor(self._GetGerritHost())
2440
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002441 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002442 """Apply a rough heuristic to give a simple summary of an issue's review
2443 or CQ status, assuming adherence to a common workflow.
2444
2445 Returns None if no issue for this branch, or one of the following keywords:
2446 * 'error' - error from review tool (including deleted issues)
2447 * 'unsent' - no reviewers added
2448 * 'waiting' - waiting for review
2449 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002450 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2451 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002452 * 'commit' - in the commit queue
2453 * 'closed' - abandoned
2454 """
2455 if not self.GetIssue():
2456 return None
2457
2458 try:
2459 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002460 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002461 return 'error'
2462
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002463 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002464 return 'closed'
2465
2466 cq_label = data['labels'].get('Commit-Queue', {})
2467 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002468 votes = cq_label.get('all', [])
2469 highest_vote = 0
2470 for v in votes:
2471 highest_vote = max(highest_vote, v.get('value', 0))
2472 vote_value = str(highest_vote)
2473 if vote_value != '0':
2474 # Add a '+' if the value is not 0 to match the values in the label.
2475 # The cq_label does not have negatives.
2476 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002477 vote_text = cq_label.get('values', {}).get(vote_value, '')
2478 if vote_text.lower() == 'commit':
2479 return 'commit'
2480
2481 lgtm_label = data['labels'].get('Code-Review', {})
2482 if lgtm_label:
2483 if 'rejected' in lgtm_label:
2484 return 'not lgtm'
2485 if 'approved' in lgtm_label:
2486 return 'lgtm'
2487
2488 if not data.get('reviewers', {}).get('REVIEWER', []):
2489 return 'unsent'
2490
2491 messages = data.get('messages', [])
2492 if messages:
2493 owner = data['owner'].get('_account_id')
2494 last_message_author = messages[-1].get('author', {}).get('_account_id')
2495 if owner != last_message_author:
2496 # Some reply from non-owner.
2497 return 'reply'
2498
2499 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002500
2501 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002502 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002503 return data['revisions'][data['current_revision']]['_number']
2504
2505 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002506 data = self._GetChangeDetail(['CURRENT_REVISION'])
2507 current_rev = data['current_revision']
2508 url = data['revisions'][current_rev]['fetch']['http']['url']
2509 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002510
dsansomee2d6fd92016-09-08 00:10:47 -07002511 def UpdateDescriptionRemote(self, description, force=False):
2512 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2513 if not force:
2514 ask_for_data(
2515 'The description cannot be modified while the issue has a pending '
2516 'unpublished edit. Either publish the edit in the Gerrit web UI '
2517 'or delete it.\n\n'
2518 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2519
2520 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2521 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002522 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2523 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002524
2525 def CloseIssue(self):
2526 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2527
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002528 def GetApprovingReviewers(self):
2529 """Returns a list of reviewers approving the change.
2530
2531 Note: not necessarily committers.
2532 """
2533 raise NotImplementedError()
2534
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002535 def SubmitIssue(self, wait_for_merge=True):
2536 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2537 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002538
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002539 def _GetChangeDetail(self, options=None, issue=None):
2540 options = options or []
2541 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002542 assert issue, 'issue is required to query Gerrit'
2543 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002544 options)
tandriic2405f52016-10-10 08:13:15 -07002545 if not data:
2546 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2547 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002548
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002549 def CMDLand(self, force, bypass_hooks, verbose):
2550 if git_common.is_dirty_git_tree('land'):
2551 return 1
tandriid60367b2016-06-22 05:25:12 -07002552 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2553 if u'Commit-Queue' in detail.get('labels', {}):
2554 if not force:
2555 ask_for_data('\nIt seems this repository has a Commit Queue, '
2556 'which can test and land changes for you. '
2557 'Are you sure you wish to bypass it?\n'
2558 'Press Enter to continue, Ctrl+C to abort.')
2559
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002560 differs = True
tandriic4344b52016-08-29 06:04:54 -07002561 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002562 # Note: git diff outputs nothing if there is no diff.
2563 if not last_upload or RunGit(['diff', last_upload]).strip():
2564 print('WARNING: some changes from local branch haven\'t been uploaded')
2565 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002566 if detail['current_revision'] == last_upload:
2567 differs = False
2568 else:
2569 print('WARNING: local branch contents differ from latest uploaded '
2570 'patchset')
2571 if differs:
2572 if not force:
2573 ask_for_data(
2574 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2575 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2576 elif not bypass_hooks:
2577 hook_results = self.RunHook(
2578 committing=True,
2579 may_prompt=not force,
2580 verbose=verbose,
2581 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2582 if not hook_results.should_continue():
2583 return 1
2584
2585 self.SubmitIssue(wait_for_merge=True)
2586 print('Issue %s has been submitted.' % self.GetIssueURL())
2587 return 0
2588
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002589 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2590 directory):
2591 assert not reject
2592 assert not nocommit
2593 assert not directory
2594 assert parsed_issue_arg.valid
2595
2596 self._changelist.issue = parsed_issue_arg.issue
2597
2598 if parsed_issue_arg.hostname:
2599 self._gerrit_host = parsed_issue_arg.hostname
2600 self._gerrit_server = 'https://%s' % self._gerrit_host
2601
tandriic2405f52016-10-10 08:13:15 -07002602 try:
2603 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2604 except GerritIssueNotExists as e:
2605 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002606
2607 if not parsed_issue_arg.patchset:
2608 # Use current revision by default.
2609 revision_info = detail['revisions'][detail['current_revision']]
2610 patchset = int(revision_info['_number'])
2611 else:
2612 patchset = parsed_issue_arg.patchset
2613 for revision_info in detail['revisions'].itervalues():
2614 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2615 break
2616 else:
2617 DieWithError('Couldn\'t find patchset %i in issue %i' %
2618 (parsed_issue_arg.patchset, self.GetIssue()))
2619
2620 fetch_info = revision_info['fetch']['http']
2621 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2622 RunGit(['cherry-pick', 'FETCH_HEAD'])
2623 self.SetIssue(self.GetIssue())
2624 self.SetPatchset(patchset)
2625 print('Committed patch for issue %i pathset %i locally' %
2626 (self.GetIssue(), self.GetPatchset()))
2627 return 0
2628
2629 @staticmethod
2630 def ParseIssueURL(parsed_url):
2631 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2632 return None
2633 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2634 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2635 # Short urls like https://domain/<issue_number> can be used, but don't allow
2636 # specifying the patchset (you'd 404), but we allow that here.
2637 if parsed_url.path == '/':
2638 part = parsed_url.fragment
2639 else:
2640 part = parsed_url.path
2641 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2642 if match:
2643 return _ParsedIssueNumberArgument(
2644 issue=int(match.group(2)),
2645 patchset=int(match.group(4)) if match.group(4) else None,
2646 hostname=parsed_url.netloc)
2647 return None
2648
tandrii16e0b4e2016-06-07 10:34:28 -07002649 def _GerritCommitMsgHookCheck(self, offer_removal):
2650 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2651 if not os.path.exists(hook):
2652 return
2653 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2654 # custom developer made one.
2655 data = gclient_utils.FileRead(hook)
2656 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2657 return
2658 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002659 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002660 'and may interfere with it in subtle ways.\n'
2661 'We recommend you remove the commit-msg hook.')
2662 if offer_removal:
2663 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2664 if reply.lower().startswith('y'):
2665 gclient_utils.rm_file_or_tree(hook)
2666 print('Gerrit commit-msg hook removed.')
2667 else:
2668 print('OK, will keep Gerrit commit-msg hook in place.')
2669
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002670 def CMDUploadChange(self, options, args, change):
2671 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002672 if options.squash and options.no_squash:
2673 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002674
2675 if not options.squash and not options.no_squash:
2676 # Load default for user, repo, squash=true, in this order.
2677 options.squash = settings.GetSquashGerritUploads()
2678 elif options.no_squash:
2679 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002680
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681 # We assume the remote called "origin" is the one we want.
2682 # It is probably not worthwhile to support different workflows.
2683 gerrit_remote = 'origin'
2684
2685 remote, remote_branch = self.GetRemoteBranch()
2686 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2687 pending_prefix='')
2688
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002690 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002691 if self.GetIssue():
2692 # Try to get the message from a previous upload.
2693 message = self.GetDescription()
2694 if not message:
2695 DieWithError(
2696 'failed to fetch description from current Gerrit issue %d\n'
2697 '%s' % (self.GetIssue(), self.GetIssueURL()))
2698 change_id = self._GetChangeDetail()['change_id']
2699 while True:
2700 footer_change_ids = git_footers.get_footer_change_id(message)
2701 if footer_change_ids == [change_id]:
2702 break
2703 if not footer_change_ids:
2704 message = git_footers.add_footer_change_id(message, change_id)
2705 print('WARNING: appended missing Change-Id to issue description')
2706 continue
2707 # There is already a valid footer but with different or several ids.
2708 # Doing this automatically is non-trivial as we don't want to lose
2709 # existing other footers, yet we want to append just 1 desired
2710 # Change-Id. Thus, just create a new footer, but let user verify the
2711 # new description.
2712 message = '%s\n\nChange-Id: %s' % (message, change_id)
2713 print(
2714 'WARNING: issue %s has Change-Id footer(s):\n'
2715 ' %s\n'
2716 'but issue has Change-Id %s, according to Gerrit.\n'
2717 'Please, check the proposed correction to the description, '
2718 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2719 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2720 change_id))
2721 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2722 if not options.force:
2723 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002724 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002725 message = change_desc.description
2726 if not message:
2727 DieWithError("Description is empty. Aborting...")
2728 # Continue the while loop.
2729 # Sanity check of this code - we should end up with proper message
2730 # footer.
2731 assert [change_id] == git_footers.get_footer_change_id(message)
2732 change_desc = ChangeDescription(message)
2733 else:
2734 change_desc = ChangeDescription(
2735 options.message or CreateDescriptionFromLog(args))
2736 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002737 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738 if not change_desc.description:
2739 DieWithError("Description is empty. Aborting...")
2740 message = change_desc.description
2741 change_ids = git_footers.get_footer_change_id(message)
2742 if len(change_ids) > 1:
2743 DieWithError('too many Change-Id footers, at most 1 allowed.')
2744 if not change_ids:
2745 # Generate the Change-Id automatically.
2746 message = git_footers.add_footer_change_id(
2747 message, GenerateGerritChangeId(message))
2748 change_desc.set_description(message)
2749 change_ids = git_footers.get_footer_change_id(message)
2750 assert len(change_ids) == 1
2751 change_id = change_ids[0]
2752
2753 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2754 if remote is '.':
2755 # If our upstream branch is local, we base our squashed commit on its
2756 # squashed version.
2757 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2758 # Check the squashed hash of the parent.
2759 parent = RunGit(['config',
2760 'branch.%s.gerritsquashhash' % upstream_branch_name],
2761 error_ok=True).strip()
2762 # Verify that the upstream branch has been uploaded too, otherwise
2763 # Gerrit will create additional CLs when uploading.
2764 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2765 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002766 DieWithError(
2767 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002768 'Note: maybe you\'ve uploaded it with --no-squash. '
2769 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 ' git cl upload --squash\n' % upstream_branch_name)
2771 else:
2772 parent = self.GetCommonAncestorWithUpstream()
2773
2774 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2775 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2776 '-m', message]).strip()
2777 else:
2778 change_desc = ChangeDescription(
2779 options.message or CreateDescriptionFromLog(args))
2780 if not change_desc.description:
2781 DieWithError("Description is empty. Aborting...")
2782
2783 if not git_footers.get_footer_change_id(change_desc.description):
2784 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002785 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2786 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002787 ref_to_push = 'HEAD'
2788 parent = '%s/%s' % (gerrit_remote, branch)
2789 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2790
2791 assert change_desc
2792 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2793 ref_to_push)]).splitlines()
2794 if len(commits) > 1:
2795 print('WARNING: This will upload %d commits. Run the following command '
2796 'to see which commits will be uploaded: ' % len(commits))
2797 print('git log %s..%s' % (parent, ref_to_push))
2798 print('You can also use `git squash-branch` to squash these into a '
2799 'single commit.')
2800 ask_for_data('About to upload; enter to confirm.')
2801
2802 if options.reviewers or options.tbr_owners:
2803 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2804 change)
2805
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002806 # Extra options that can be specified at push time. Doc:
2807 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2808 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002809 if change_desc.get_reviewers(tbr_only=True):
2810 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2811 refspec_opts.append('l=Code-Review+1')
2812
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002814 if not re.match(r'^[\w ]+$', options.title):
2815 options.title = re.sub(r'[^\w ]', '', options.title)
2816 print('WARNING: Patchset title may only contain alphanumeric chars '
2817 'and spaces. Cleaned up title:\n%s' % options.title)
2818 if not options.force:
2819 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002820 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2821 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002822 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2823
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002824 if options.send_mail:
2825 if not change_desc.get_reviewers():
2826 DieWithError('Must specify reviewers to send email.')
2827 refspec_opts.append('notify=ALL')
2828 else:
2829 refspec_opts.append('notify=NONE')
2830
tandrii99a72f22016-08-17 14:33:24 -07002831 reviewers = change_desc.get_reviewers()
2832 if reviewers:
2833 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002834
agablec6787972016-09-09 16:13:34 -07002835 if options.private:
2836 refspec_opts.append('draft')
2837
rmistry9eadede2016-09-19 11:22:43 -07002838 if options.topic:
2839 # Documentation on Gerrit topics is here:
2840 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2841 refspec_opts.append('topic=%s' % options.topic)
2842
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002843 refspec_suffix = ''
2844 if refspec_opts:
2845 refspec_suffix = '%' + ','.join(refspec_opts)
2846 assert ' ' not in refspec_suffix, (
2847 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002848 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002849
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002850 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002851 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 print_stdout=True,
2853 # Flush after every line: useful for seeing progress when running as
2854 # recipe.
2855 filter_fn=lambda _: sys.stdout.flush())
2856
2857 if options.squash:
2858 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2859 change_numbers = [m.group(1)
2860 for m in map(regex.match, push_stdout.splitlines())
2861 if m]
2862 if len(change_numbers) != 1:
2863 DieWithError(
2864 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2865 'Change-Id: %s') % (len(change_numbers), change_id))
2866 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002867 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002868
2869 # Add cc's from the CC_LIST and --cc flag (if any).
2870 cc = self.GetCCList().split(',')
2871 if options.cc:
2872 cc.extend(options.cc)
2873 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002874 if change_desc.get_cced():
2875 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002876 if cc:
2877 gerrit_util.AddReviewers(
2878 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2879
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002880 return 0
2881
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002882 def _AddChangeIdToCommitMessage(self, options, args):
2883 """Re-commits using the current message, assumes the commit hook is in
2884 place.
2885 """
2886 log_desc = options.message or CreateDescriptionFromLog(args)
2887 git_command = ['commit', '--amend', '-m', log_desc]
2888 RunGit(git_command)
2889 new_log_desc = CreateDescriptionFromLog(args)
2890 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002891 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002892 return new_log_desc
2893 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002894 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002895
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002896 def SetCQState(self, new_state):
2897 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002898 vote_map = {
2899 _CQState.NONE: 0,
2900 _CQState.DRY_RUN: 1,
2901 _CQState.COMMIT : 2,
2902 }
2903 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2904 labels={'Commit-Queue': vote_map[new_state]})
2905
tandriie113dfd2016-10-11 10:20:12 -07002906 def CannotTriggerTryJobReason(self):
2907 # TODO(tandrii): implement for Gerrit.
2908 raise NotImplementedError()
2909
tandriide281ae2016-10-12 06:02:30 -07002910 def GetIssueOwner(self):
2911 # TODO(tandrii): implement for Gerrit.
2912 raise NotImplementedError()
2913
2914 def GetIssueProject(self):
2915 # TODO(tandrii): implement for Gerrit.
2916 raise NotImplementedError()
2917
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002918
2919_CODEREVIEW_IMPLEMENTATIONS = {
2920 'rietveld': _RietveldChangelistImpl,
2921 'gerrit': _GerritChangelistImpl,
2922}
2923
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002924
iannuccie53c9352016-08-17 14:40:40 -07002925def _add_codereview_issue_select_options(parser, extra=""):
2926 _add_codereview_select_options(parser)
2927
2928 text = ('Operate on this issue number instead of the current branch\'s '
2929 'implicit issue.')
2930 if extra:
2931 text += ' '+extra
2932 parser.add_option('-i', '--issue', type=int, help=text)
2933
2934
2935def _process_codereview_issue_select_options(parser, options):
2936 _process_codereview_select_options(parser, options)
2937 if options.issue is not None and not options.forced_codereview:
2938 parser.error('--issue must be specified with either --rietveld or --gerrit')
2939
2940
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002941def _add_codereview_select_options(parser):
2942 """Appends --gerrit and --rietveld options to force specific codereview."""
2943 parser.codereview_group = optparse.OptionGroup(
2944 parser, 'EXPERIMENTAL! Codereview override options')
2945 parser.add_option_group(parser.codereview_group)
2946 parser.codereview_group.add_option(
2947 '--gerrit', action='store_true',
2948 help='Force the use of Gerrit for codereview')
2949 parser.codereview_group.add_option(
2950 '--rietveld', action='store_true',
2951 help='Force the use of Rietveld for codereview')
2952
2953
2954def _process_codereview_select_options(parser, options):
2955 if options.gerrit and options.rietveld:
2956 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2957 options.forced_codereview = None
2958 if options.gerrit:
2959 options.forced_codereview = 'gerrit'
2960 elif options.rietveld:
2961 options.forced_codereview = 'rietveld'
2962
2963
tandriif9aefb72016-07-01 09:06:51 -07002964def _get_bug_line_values(default_project, bugs):
2965 """Given default_project and comma separated list of bugs, yields bug line
2966 values.
2967
2968 Each bug can be either:
2969 * a number, which is combined with default_project
2970 * string, which is left as is.
2971
2972 This function may produce more than one line, because bugdroid expects one
2973 project per line.
2974
2975 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2976 ['v8:123', 'chromium:789']
2977 """
2978 default_bugs = []
2979 others = []
2980 for bug in bugs.split(','):
2981 bug = bug.strip()
2982 if bug:
2983 try:
2984 default_bugs.append(int(bug))
2985 except ValueError:
2986 others.append(bug)
2987
2988 if default_bugs:
2989 default_bugs = ','.join(map(str, default_bugs))
2990 if default_project:
2991 yield '%s:%s' % (default_project, default_bugs)
2992 else:
2993 yield default_bugs
2994 for other in sorted(others):
2995 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2996 yield other
2997
2998
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002999class ChangeDescription(object):
3000 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003001 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003002 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003003 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003004
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003007
agable@chromium.org42c20792013-09-12 17:34:49 +00003008 @property # www.logilab.org/ticket/89786
3009 def description(self): # pylint: disable=E0202
3010 return '\n'.join(self._description_lines)
3011
3012 def set_description(self, desc):
3013 if isinstance(desc, basestring):
3014 lines = desc.splitlines()
3015 else:
3016 lines = [line.rstrip() for line in desc]
3017 while lines and not lines[0]:
3018 lines.pop(0)
3019 while lines and not lines[-1]:
3020 lines.pop(-1)
3021 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003022
piman@chromium.org336f9122014-09-04 02:16:55 +00003023 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003024 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003026 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003029
agable@chromium.org42c20792013-09-12 17:34:49 +00003030 # Get the set of R= and TBR= lines and remove them from the desciption.
3031 regexp = re.compile(self.R_LINE)
3032 matches = [regexp.match(line) for line in self._description_lines]
3033 new_desc = [l for i, l in enumerate(self._description_lines)
3034 if not matches[i]]
3035 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003036
agable@chromium.org42c20792013-09-12 17:34:49 +00003037 # Construct new unified R= and TBR= lines.
3038 r_names = []
3039 tbr_names = []
3040 for match in matches:
3041 if not match:
3042 continue
3043 people = cleanup_list([match.group(2).strip()])
3044 if match.group(1) == 'TBR':
3045 tbr_names.extend(people)
3046 else:
3047 r_names.extend(people)
3048 for name in r_names:
3049 if name not in reviewers:
3050 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003051 if add_owners_tbr:
3052 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003053 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003054 all_reviewers = set(tbr_names + reviewers)
3055 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3056 all_reviewers)
3057 tbr_names.extend(owners_db.reviewers_for(missing_files,
3058 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003059 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3060 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3061
3062 # Put the new lines in the description where the old first R= line was.
3063 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3064 if 0 <= line_loc < len(self._description_lines):
3065 if new_tbr_line:
3066 self._description_lines.insert(line_loc, new_tbr_line)
3067 if new_r_line:
3068 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003070 if new_r_line:
3071 self.append_footer(new_r_line)
3072 if new_tbr_line:
3073 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074
tandriif9aefb72016-07-01 09:06:51 -07003075 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003076 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003077 self.set_description([
3078 '# Enter a description of the change.',
3079 '# This will be displayed on the codereview site.',
3080 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003081 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003082 '--------------------',
3083 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003084
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 regexp = re.compile(self.BUG_LINE)
3086 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003087 prefix = settings.GetBugPrefix()
3088 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3089 for value in values:
3090 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3091 self.append_footer('BUG=%s' % value)
3092
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003094 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003095 if not content:
3096 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003097 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098
3099 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3101 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003102 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003103 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003104
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003105 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003106 """Adds a footer line to the description.
3107
3108 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3109 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3110 that Gerrit footers are always at the end.
3111 """
3112 parsed_footer_line = git_footers.parse_footer(line)
3113 if parsed_footer_line:
3114 # Line is a gerrit footer in the form: Footer-Key: any value.
3115 # Thus, must be appended observing Gerrit footer rules.
3116 self.set_description(
3117 git_footers.add_footer(self.description,
3118 key=parsed_footer_line[0],
3119 value=parsed_footer_line[1]))
3120 return
3121
3122 if not self._description_lines:
3123 self._description_lines.append(line)
3124 return
3125
3126 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3127 if gerrit_footers:
3128 # git_footers.split_footers ensures that there is an empty line before
3129 # actual (gerrit) footers, if any. We have to keep it that way.
3130 assert top_lines and top_lines[-1] == ''
3131 top_lines, separator = top_lines[:-1], top_lines[-1:]
3132 else:
3133 separator = [] # No need for separator if there are no gerrit_footers.
3134
3135 prev_line = top_lines[-1] if top_lines else ''
3136 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3137 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3138 top_lines.append('')
3139 top_lines.append(line)
3140 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003141
tandrii99a72f22016-08-17 14:33:24 -07003142 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003144 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003145 reviewers = [match.group(2).strip()
3146 for match in matches
3147 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003148 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003149
bradnelsond975b302016-10-23 12:20:23 -07003150 def get_cced(self):
3151 """Retrieves the list of reviewers."""
3152 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3153 cced = [match.group(2).strip() for match in matches if match]
3154 return cleanup_list(cced)
3155
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003156
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003157def get_approving_reviewers(props):
3158 """Retrieves the reviewers that approved a CL from the issue properties with
3159 messages.
3160
3161 Note that the list may contain reviewers that are not committer, thus are not
3162 considered by the CQ.
3163 """
3164 return sorted(
3165 set(
3166 message['sender']
3167 for message in props['messages']
3168 if message['approval'] and message['sender'] in props['reviewers']
3169 )
3170 )
3171
3172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173def FindCodereviewSettingsFile(filename='codereview.settings'):
3174 """Finds the given file starting in the cwd and going up.
3175
3176 Only looks up to the top of the repository unless an
3177 'inherit-review-settings-ok' file exists in the root of the repository.
3178 """
3179 inherit_ok_file = 'inherit-review-settings-ok'
3180 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003181 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003182 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3183 root = '/'
3184 while True:
3185 if filename in os.listdir(cwd):
3186 if os.path.isfile(os.path.join(cwd, filename)):
3187 return open(os.path.join(cwd, filename))
3188 if cwd == root:
3189 break
3190 cwd = os.path.dirname(cwd)
3191
3192
3193def LoadCodereviewSettingsFromFile(fileobj):
3194 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003195 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003197 def SetProperty(name, setting, unset_error_ok=False):
3198 fullname = 'rietveld.' + name
3199 if setting in keyvals:
3200 RunGit(['config', fullname, keyvals[setting]])
3201 else:
3202 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3203
tandrii48df5812016-10-17 03:55:37 -07003204 if not keyvals.get('GERRIT_HOST', False):
3205 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206 # Only server setting is required. Other settings can be absent.
3207 # In that case, we ignore errors raised during option deletion attempt.
3208 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003209 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003210 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3211 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003212 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003213 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003214 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3215 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003216 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003217 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003218 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003219 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003220 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3221 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003222
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003223 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003224 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003225
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003226 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003227 RunGit(['config', 'gerrit.squash-uploads',
3228 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003229
tandrii@chromium.org28253532016-04-14 13:46:56 +00003230 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003231 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003232 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003234 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3235 #should be of the form
3236 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3237 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3238 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3239 keyvals['ORIGIN_URL_CONFIG']])
3240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003242def urlretrieve(source, destination):
3243 """urllib is broken for SSL connections via a proxy therefore we
3244 can't use urllib.urlretrieve()."""
3245 with open(destination, 'w') as f:
3246 f.write(urllib2.urlopen(source).read())
3247
3248
ukai@chromium.org712d6102013-11-27 00:52:58 +00003249def hasSheBang(fname):
3250 """Checks fname is a #! script."""
3251 with open(fname) as f:
3252 return f.read(2).startswith('#!')
3253
3254
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003255# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3256def DownloadHooks(*args, **kwargs):
3257 pass
3258
3259
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003260def DownloadGerritHook(force):
3261 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003262
3263 Args:
3264 force: True to update hooks. False to install hooks if not present.
3265 """
3266 if not settings.GetIsGerrit():
3267 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003268 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003269 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3270 if not os.access(dst, os.X_OK):
3271 if os.path.exists(dst):
3272 if not force:
3273 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003274 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003275 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003276 if not hasSheBang(dst):
3277 DieWithError('Not a script: %s\n'
3278 'You need to download from\n%s\n'
3279 'into .git/hooks/commit-msg and '
3280 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003281 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3282 except Exception:
3283 if os.path.exists(dst):
3284 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003285 DieWithError('\nFailed to download hooks.\n'
3286 'You need to download from\n%s\n'
3287 'into .git/hooks/commit-msg and '
3288 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003289
3290
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003291
3292def GetRietveldCodereviewSettingsInteractively():
3293 """Prompt the user for settings."""
3294 server = settings.GetDefaultServerUrl(error_ok=True)
3295 prompt = 'Rietveld server (host[:port])'
3296 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3297 newserver = ask_for_data(prompt + ':')
3298 if not server and not newserver:
3299 newserver = DEFAULT_SERVER
3300 if newserver:
3301 newserver = gclient_utils.UpgradeToHttps(newserver)
3302 if newserver != server:
3303 RunGit(['config', 'rietveld.server', newserver])
3304
3305 def SetProperty(initial, caption, name, is_url):
3306 prompt = caption
3307 if initial:
3308 prompt += ' ("x" to clear) [%s]' % initial
3309 new_val = ask_for_data(prompt + ':')
3310 if new_val == 'x':
3311 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3312 elif new_val:
3313 if is_url:
3314 new_val = gclient_utils.UpgradeToHttps(new_val)
3315 if new_val != initial:
3316 RunGit(['config', 'rietveld.' + name, new_val])
3317
3318 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3319 SetProperty(settings.GetDefaultPrivateFlag(),
3320 'Private flag (rietveld only)', 'private', False)
3321 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3322 'tree-status-url', False)
3323 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3324 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3325 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3326 'run-post-upload-hook', False)
3327
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003328@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003329def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003330 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003331
tandrii5d0a0422016-09-14 06:24:35 -07003332 print('WARNING: git cl config works for Rietveld only')
3333 # TODO(tandrii): remove this once we switch to Gerrit.
3334 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003335 parser.add_option('--activate-update', action='store_true',
3336 help='activate auto-updating [rietveld] section in '
3337 '.git/config')
3338 parser.add_option('--deactivate-update', action='store_true',
3339 help='deactivate auto-updating [rietveld] section in '
3340 '.git/config')
3341 options, args = parser.parse_args(args)
3342
3343 if options.deactivate_update:
3344 RunGit(['config', 'rietveld.autoupdate', 'false'])
3345 return
3346
3347 if options.activate_update:
3348 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3349 return
3350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003351 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003352 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353 return 0
3354
3355 url = args[0]
3356 if not url.endswith('codereview.settings'):
3357 url = os.path.join(url, 'codereview.settings')
3358
3359 # Load code review settings and download hooks (if available).
3360 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3361 return 0
3362
3363
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003364def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003365 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003366 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3367 branch = ShortBranchName(branchref)
3368 _, args = parser.parse_args(args)
3369 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003370 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003371 return RunGit(['config', 'branch.%s.base-url' % branch],
3372 error_ok=False).strip()
3373 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003374 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003375 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3376 error_ok=False).strip()
3377
3378
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003379def color_for_status(status):
3380 """Maps a Changelist status to color, for CMDstatus and other tools."""
3381 return {
3382 'unsent': Fore.RED,
3383 'waiting': Fore.BLUE,
3384 'reply': Fore.YELLOW,
3385 'lgtm': Fore.GREEN,
3386 'commit': Fore.MAGENTA,
3387 'closed': Fore.CYAN,
3388 'error': Fore.WHITE,
3389 }.get(status, Fore.WHITE)
3390
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003391
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003392def get_cl_statuses(changes, fine_grained, max_processes=None):
3393 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003394
3395 If fine_grained is true, this will fetch CL statuses from the server.
3396 Otherwise, simply indicate if there's a matching url for the given branches.
3397
3398 If max_processes is specified, it is used as the maximum number of processes
3399 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3400 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003401
3402 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003403 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003404 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003405 upload.verbosity = 0
3406
3407 if fine_grained:
3408 # Process one branch synchronously to work through authentication, then
3409 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003410 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003411 def fetch(cl):
3412 try:
3413 return (cl, cl.GetStatus())
3414 except:
3415 # See http://crbug.com/629863.
3416 logging.exception('failed to fetch status for %s:', cl)
3417 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003418 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003419
tandriiea9514a2016-08-17 12:32:37 -07003420 changes_to_fetch = changes[1:]
3421 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003422 # Exit early if there was only one branch to fetch.
3423 return
3424
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003425 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003426 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003427 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003428 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003429
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003430 fetched_cls = set()
3431 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003432 while True:
3433 try:
3434 row = it.next(timeout=5)
3435 except multiprocessing.TimeoutError:
3436 break
3437
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003438 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003439 yield row
3440
3441 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003442 for cl in set(changes_to_fetch) - fetched_cls:
3443 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003444
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003445 else:
3446 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003447 for cl in changes:
3448 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003449
rmistry@google.com2dd99862015-06-22 12:22:18 +00003450
3451def upload_branch_deps(cl, args):
3452 """Uploads CLs of local branches that are dependents of the current branch.
3453
3454 If the local branch dependency tree looks like:
3455 test1 -> test2.1 -> test3.1
3456 -> test3.2
3457 -> test2.2 -> test3.3
3458
3459 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3460 run on the dependent branches in this order:
3461 test2.1, test3.1, test3.2, test2.2, test3.3
3462
3463 Note: This function does not rebase your local dependent branches. Use it when
3464 you make a change to the parent branch that will not conflict with its
3465 dependent branches, and you would like their dependencies updated in
3466 Rietveld.
3467 """
3468 if git_common.is_dirty_git_tree('upload-branch-deps'):
3469 return 1
3470
3471 root_branch = cl.GetBranch()
3472 if root_branch is None:
3473 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3474 'Get on a branch!')
3475 if not cl.GetIssue() or not cl.GetPatchset():
3476 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3477 'patchset dependencies without an uploaded CL.')
3478
3479 branches = RunGit(['for-each-ref',
3480 '--format=%(refname:short) %(upstream:short)',
3481 'refs/heads'])
3482 if not branches:
3483 print('No local branches found.')
3484 return 0
3485
3486 # Create a dictionary of all local branches to the branches that are dependent
3487 # on it.
3488 tracked_to_dependents = collections.defaultdict(list)
3489 for b in branches.splitlines():
3490 tokens = b.split()
3491 if len(tokens) == 2:
3492 branch_name, tracked = tokens
3493 tracked_to_dependents[tracked].append(branch_name)
3494
vapiera7fbd5a2016-06-16 09:17:49 -07003495 print()
3496 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003497 dependents = []
3498 def traverse_dependents_preorder(branch, padding=''):
3499 dependents_to_process = tracked_to_dependents.get(branch, [])
3500 padding += ' '
3501 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003502 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003503 dependents.append(dependent)
3504 traverse_dependents_preorder(dependent, padding)
3505 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003506 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003507
3508 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003509 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003510 return 0
3511
vapiera7fbd5a2016-06-16 09:17:49 -07003512 print('This command will checkout all dependent branches and run '
3513 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003514 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3515
andybons@chromium.org962f9462016-02-03 20:00:42 +00003516 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003517 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003518 args.extend(['-t', 'Updated patchset dependency'])
3519
rmistry@google.com2dd99862015-06-22 12:22:18 +00003520 # Record all dependents that failed to upload.
3521 failures = {}
3522 # Go through all dependents, checkout the branch and upload.
3523 try:
3524 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003525 print()
3526 print('--------------------------------------')
3527 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003528 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003529 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003530 try:
3531 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003533 failures[dependent_branch] = 1
3534 except: # pylint: disable=W0702
3535 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003536 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003537 finally:
3538 # Swap back to the original root branch.
3539 RunGit(['checkout', '-q', root_branch])
3540
vapiera7fbd5a2016-06-16 09:17:49 -07003541 print()
3542 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003543 for dependent_branch in dependents:
3544 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print(' %s : %s' % (dependent_branch, upload_status))
3546 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003547
3548 return 0
3549
3550
kmarshall3bff56b2016-06-06 18:31:47 -07003551def CMDarchive(parser, args):
3552 """Archives and deletes branches associated with closed changelists."""
3553 parser.add_option(
3554 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003555 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003556 parser.add_option(
3557 '-f', '--force', action='store_true',
3558 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003559 parser.add_option(
3560 '-d', '--dry-run', action='store_true',
3561 help='Skip the branch tagging and removal steps.')
3562 parser.add_option(
3563 '-t', '--notags', action='store_true',
3564 help='Do not tag archived branches. '
3565 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003566
3567 auth.add_auth_options(parser)
3568 options, args = parser.parse_args(args)
3569 if args:
3570 parser.error('Unsupported args: %s' % ' '.join(args))
3571 auth_config = auth.extract_auth_config_from_options(options)
3572
3573 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3574 if not branches:
3575 return 0
3576
vapiera7fbd5a2016-06-16 09:17:49 -07003577 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003578 changes = [Changelist(branchref=b, auth_config=auth_config)
3579 for b in branches.splitlines()]
3580 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3581 statuses = get_cl_statuses(changes,
3582 fine_grained=True,
3583 max_processes=options.maxjobs)
3584 proposal = [(cl.GetBranch(),
3585 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3586 for cl, status in statuses
3587 if status == 'closed']
3588 proposal.sort()
3589
3590 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003592 return 0
3593
3594 current_branch = GetCurrentBranch()
3595
vapiera7fbd5a2016-06-16 09:17:49 -07003596 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003597 if options.notags:
3598 for next_item in proposal:
3599 print(' ' + next_item[0])
3600 else:
3601 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3602 for next_item in proposal:
3603 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003604
kmarshall9249e012016-08-23 12:02:16 -07003605 # Quit now on precondition failure or if instructed by the user, either
3606 # via an interactive prompt or by command line flags.
3607 if options.dry_run:
3608 print('\nNo changes were made (dry run).\n')
3609 return 0
3610 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003611 print('You are currently on a branch \'%s\' which is associated with a '
3612 'closed codereview issue, so archive cannot proceed. Please '
3613 'checkout another branch and run this command again.' %
3614 current_branch)
3615 return 1
kmarshall9249e012016-08-23 12:02:16 -07003616 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003617 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3618 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003619 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003620 return 1
3621
3622 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003623 if not options.notags:
3624 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003625 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003626
vapiera7fbd5a2016-06-16 09:17:49 -07003627 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003628
3629 return 0
3630
3631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003633 """Show status of changelists.
3634
3635 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003636 - Red not sent for review or broken
3637 - Blue waiting for review
3638 - Yellow waiting for you to reply to review
3639 - Green LGTM'ed
3640 - Magenta in the commit queue
3641 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003642
3643 Also see 'git cl comments'.
3644 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003646 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003647 parser.add_option('-f', '--fast', action='store_true',
3648 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003649 parser.add_option(
3650 '-j', '--maxjobs', action='store', type=int,
3651 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003652
3653 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003654 _add_codereview_issue_select_options(
3655 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003656 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003657 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003658 if args:
3659 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003660 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661
iannuccie53c9352016-08-17 14:40:40 -07003662 if options.issue is not None and not options.field:
3663 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003666 cl = Changelist(auth_config=auth_config, issue=options.issue,
3667 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003668 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003669 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003670 elif options.field == 'id':
3671 issueid = cl.GetIssue()
3672 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003673 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003674 elif options.field == 'patch':
3675 patchset = cl.GetPatchset()
3676 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003678 elif options.field == 'status':
3679 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003680 elif options.field == 'url':
3681 url = cl.GetIssueURL()
3682 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003683 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003684 return 0
3685
3686 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3687 if not branches:
3688 print('No local branch found.')
3689 return 0
3690
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003691 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003692 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003693 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003695 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003696 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003697 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003698
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003699 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003700 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3701 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3702 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003703 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003704 c, status = output.next()
3705 branch_statuses[c.GetBranch()] = status
3706 status = branch_statuses.pop(branch)
3707 url = cl.GetIssueURL()
3708 if url and (not status or status == 'error'):
3709 # The issue probably doesn't exist anymore.
3710 url += ' (broken)'
3711
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003712 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003713 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003714 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003715 color = ''
3716 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003717 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003719 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003720 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003721
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003722 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print()
3724 print('Current branch:',)
3725 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003726 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003727 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003728 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003729 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003730 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print('Issue description:')
3732 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003733 return 0
3734
3735
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003736def colorize_CMDstatus_doc():
3737 """To be called once in main() to add colors to git cl status help."""
3738 colors = [i for i in dir(Fore) if i[0].isupper()]
3739
3740 def colorize_line(line):
3741 for color in colors:
3742 if color in line.upper():
3743 # Extract whitespaces first and the leading '-'.
3744 indent = len(line) - len(line.lstrip(' ')) + 1
3745 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3746 return line
3747
3748 lines = CMDstatus.__doc__.splitlines()
3749 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3750
3751
phajdan.jre328cf92016-08-22 04:12:17 -07003752def write_json(path, contents):
3753 with open(path, 'w') as f:
3754 json.dump(contents, f)
3755
3756
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003757@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003759 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003760
3761 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003762 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003763 parser.add_option('-r', '--reverse', action='store_true',
3764 help='Lookup the branch(es) for the specified issues. If '
3765 'no issues are specified, all branches with mapped '
3766 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003767 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003768 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003769 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003770 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771
dnj@chromium.org406c4402015-03-03 17:22:28 +00003772 if options.reverse:
3773 branches = RunGit(['for-each-ref', 'refs/heads',
3774 '--format=%(refname:short)']).splitlines()
3775
3776 # Reverse issue lookup.
3777 issue_branch_map = {}
3778 for branch in branches:
3779 cl = Changelist(branchref=branch)
3780 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3781 if not args:
3782 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003783 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003784 for issue in args:
3785 if not issue:
3786 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003787 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print('Branch for issue number %s: %s' % (
3789 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003790 if options.json:
3791 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003792 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003793 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003794 if len(args) > 0:
3795 try:
3796 issue = int(args[0])
3797 except ValueError:
3798 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003799 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003800 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003801 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003802 if options.json:
3803 write_json(options.json, {
3804 'issue': cl.GetIssue(),
3805 'issue_url': cl.GetIssueURL(),
3806 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003807 return 0
3808
3809
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003810def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003811 """Shows or posts review comments for any changelist."""
3812 parser.add_option('-a', '--add-comment', dest='comment',
3813 help='comment to add to an issue')
3814 parser.add_option('-i', dest='issue',
3815 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003816 parser.add_option('-j', '--json-file',
3817 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003818 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003819 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003820 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003821
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003822 issue = None
3823 if options.issue:
3824 try:
3825 issue = int(options.issue)
3826 except ValueError:
3827 DieWithError('A review issue id is expected to be a number')
3828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003829 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003830
3831 if options.comment:
3832 cl.AddComment(options.comment)
3833 return 0
3834
3835 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003836 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003837 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003838 summary.append({
3839 'date': message['date'],
3840 'lgtm': False,
3841 'message': message['text'],
3842 'not_lgtm': False,
3843 'sender': message['sender'],
3844 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003845 if message['disapproval']:
3846 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003847 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003848 elif message['approval']:
3849 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003850 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003851 elif message['sender'] == data['owner_email']:
3852 color = Fore.MAGENTA
3853 else:
3854 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003855 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003856 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003857 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003858 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003860 if options.json_file:
3861 with open(options.json_file, 'wb') as f:
3862 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003863 return 0
3864
3865
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003866@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003867def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003868 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003869 parser.add_option('-d', '--display', action='store_true',
3870 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003871 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003872 help='New description to set for this issue (- for stdin, '
3873 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003874 parser.add_option('-f', '--force', action='store_true',
3875 help='Delete any unpublished Gerrit edits for this issue '
3876 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003877
3878 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003879 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003880 options, args = parser.parse_args(args)
3881 _process_codereview_select_options(parser, options)
3882
3883 target_issue = None
3884 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003885 target_issue = ParseIssueNumberArgument(args[0])
3886 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003887 parser.print_help()
3888 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003889
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003890 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003891
martiniss6eda05f2016-06-30 10:18:35 -07003892 kwargs = {
3893 'auth_config': auth_config,
3894 'codereview': options.forced_codereview,
3895 }
3896 if target_issue:
3897 kwargs['issue'] = target_issue.issue
3898 if options.forced_codereview == 'rietveld':
3899 kwargs['rietveld_server'] = target_issue.hostname
3900
3901 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003902
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003903 if not cl.GetIssue():
3904 DieWithError('This branch has no associated changelist.')
3905 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003906
smut@google.com34fb6b12015-07-13 20:03:26 +00003907 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003909 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003910
3911 if options.new_description:
3912 text = options.new_description
3913 if text == '-':
3914 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003915 elif text == '+':
3916 base_branch = cl.GetCommonAncestorWithUpstream()
3917 change = cl.GetChange(base_branch, None, local_description=True)
3918 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003919
3920 description.set_description(text)
3921 else:
3922 description.prompt()
3923
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003924 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003925 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003926 return 0
3927
3928
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003929def CreateDescriptionFromLog(args):
3930 """Pulls out the commit log to use as a base for the CL description."""
3931 log_args = []
3932 if len(args) == 1 and not args[0].endswith('.'):
3933 log_args = [args[0] + '..']
3934 elif len(args) == 1 and args[0].endswith('...'):
3935 log_args = [args[0][:-1]]
3936 elif len(args) == 2:
3937 log_args = [args[0] + '..' + args[1]]
3938 else:
3939 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003940 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941
3942
thestig@chromium.org44202a22014-03-11 19:22:18 +00003943def CMDlint(parser, args):
3944 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003945 parser.add_option('--filter', action='append', metavar='-x,+y',
3946 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003947 auth.add_auth_options(parser)
3948 options, args = parser.parse_args(args)
3949 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003950
3951 # Access to a protected member _XX of a client class
3952 # pylint: disable=W0212
3953 try:
3954 import cpplint
3955 import cpplint_chromium
3956 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003957 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003958 return 1
3959
3960 # Change the current working directory before calling lint so that it
3961 # shows the correct base.
3962 previous_cwd = os.getcwd()
3963 os.chdir(settings.GetRoot())
3964 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003965 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003966 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3967 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003968 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003969 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003970 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003971
3972 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003973 command = args + files
3974 if options.filter:
3975 command = ['--filter=' + ','.join(options.filter)] + command
3976 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003977
3978 white_regex = re.compile(settings.GetLintRegex())
3979 black_regex = re.compile(settings.GetLintIgnoreRegex())
3980 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3981 for filename in filenames:
3982 if white_regex.match(filename):
3983 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003984 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003985 else:
3986 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3987 extra_check_functions)
3988 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003990 finally:
3991 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003992 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003993 if cpplint._cpplint_state.error_count != 0:
3994 return 1
3995 return 0
3996
3997
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003999 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004000 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004002 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004003 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004004 auth.add_auth_options(parser)
4005 options, args = parser.parse_args(args)
4006 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007
sbc@chromium.org71437c02015-04-09 19:29:40 +00004008 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004009 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010 return 1
4011
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004012 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013 if args:
4014 base_branch = args[0]
4015 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004016 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004017 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004018
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004019 cl.RunHook(
4020 committing=not options.upload,
4021 may_prompt=False,
4022 verbose=options.verbose,
4023 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004024 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004025
4026
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004027def GenerateGerritChangeId(message):
4028 """Returns Ixxxxxx...xxx change id.
4029
4030 Works the same way as
4031 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4032 but can be called on demand on all platforms.
4033
4034 The basic idea is to generate git hash of a state of the tree, original commit
4035 message, author/committer info and timestamps.
4036 """
4037 lines = []
4038 tree_hash = RunGitSilent(['write-tree'])
4039 lines.append('tree %s' % tree_hash.strip())
4040 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4041 if code == 0:
4042 lines.append('parent %s' % parent.strip())
4043 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4044 lines.append('author %s' % author.strip())
4045 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4046 lines.append('committer %s' % committer.strip())
4047 lines.append('')
4048 # Note: Gerrit's commit-hook actually cleans message of some lines and
4049 # whitespace. This code is not doing this, but it clearly won't decrease
4050 # entropy.
4051 lines.append(message)
4052 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4053 stdin='\n'.join(lines))
4054 return 'I%s' % change_hash.strip()
4055
4056
wittman@chromium.org455dc922015-01-26 20:15:50 +00004057def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4058 """Computes the remote branch ref to use for the CL.
4059
4060 Args:
4061 remote (str): The git remote for the CL.
4062 remote_branch (str): The git remote branch for the CL.
4063 target_branch (str): The target branch specified by the user.
4064 pending_prefix (str): The pending prefix from the settings.
4065 """
4066 if not (remote and remote_branch):
4067 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004068
wittman@chromium.org455dc922015-01-26 20:15:50 +00004069 if target_branch:
4070 # Cannonicalize branch references to the equivalent local full symbolic
4071 # refs, which are then translated into the remote full symbolic refs
4072 # below.
4073 if '/' not in target_branch:
4074 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4075 else:
4076 prefix_replacements = (
4077 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4078 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4079 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4080 )
4081 match = None
4082 for regex, replacement in prefix_replacements:
4083 match = re.search(regex, target_branch)
4084 if match:
4085 remote_branch = target_branch.replace(match.group(0), replacement)
4086 break
4087 if not match:
4088 # This is a branch path but not one we recognize; use as-is.
4089 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004090 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4091 # Handle the refs that need to land in different refs.
4092 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004093
wittman@chromium.org455dc922015-01-26 20:15:50 +00004094 # Create the true path to the remote branch.
4095 # Does the following translation:
4096 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4097 # * refs/remotes/origin/master -> refs/heads/master
4098 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4099 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4100 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4101 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4102 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4103 'refs/heads/')
4104 elif remote_branch.startswith('refs/remotes/branch-heads'):
4105 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4106 # If a pending prefix exists then replace refs/ with it.
4107 if pending_prefix:
4108 remote_branch = remote_branch.replace('refs/', pending_prefix)
4109 return remote_branch
4110
4111
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004112def cleanup_list(l):
4113 """Fixes a list so that comma separated items are put as individual items.
4114
4115 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4116 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4117 """
4118 items = sum((i.split(',') for i in l), [])
4119 stripped_items = (i.strip() for i in items)
4120 return sorted(filter(None, stripped_items))
4121
4122
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004123@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004124def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004125 """Uploads the current changelist to codereview.
4126
4127 Can skip dependency patchset uploads for a branch by running:
4128 git config branch.branch_name.skip-deps-uploads True
4129 To unset run:
4130 git config --unset branch.branch_name.skip-deps-uploads
4131 Can also set the above globally by using the --global flag.
4132 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004133 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4134 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004135 parser.add_option('--bypass-watchlists', action='store_true',
4136 dest='bypass_watchlists',
4137 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004138 parser.add_option('-f', action='store_true', dest='force',
4139 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004140 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004141 parser.add_option('-b', '--bug',
4142 help='pre-populate the bug number(s) for this issue. '
4143 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004144 parser.add_option('--message-file', dest='message_file',
4145 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004146 parser.add_option('-t', dest='title',
4147 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004148 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004149 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004150 help='reviewer email addresses')
4151 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004152 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004153 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004154 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004155 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004156 parser.add_option('--emulate_svn_auto_props',
4157 '--emulate-svn-auto-props',
4158 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004159 dest="emulate_svn_auto_props",
4160 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004161 parser.add_option('-c', '--use-commit-queue', action='store_true',
4162 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004163 parser.add_option('--private', action='store_true',
4164 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004165 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004166 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004167 metavar='TARGET',
4168 help='Apply CL to remote ref TARGET. ' +
4169 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004170 parser.add_option('--squash', action='store_true',
4171 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004172 parser.add_option('--no-squash', action='store_true',
4173 help='Don\'t squash multiple commits into one ' +
4174 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004175 parser.add_option('--topic', default=None,
4176 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004177 parser.add_option('--email', default=None,
4178 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004179 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4180 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004181 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4182 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004183 help='Send the patchset to do a CQ dry run right after '
4184 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004185 parser.add_option('--dependencies', action='store_true',
4186 help='Uploads CLs of all the local branches that depend on '
4187 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004188
rmistry@google.com2dd99862015-06-22 12:22:18 +00004189 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004190 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004191 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004192 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004193 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004194 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004195 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004196
sbc@chromium.org71437c02015-04-09 19:29:40 +00004197 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004198 return 1
4199
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004200 options.reviewers = cleanup_list(options.reviewers)
4201 options.cc = cleanup_list(options.cc)
4202
tandriib80458a2016-06-23 12:20:07 -07004203 if options.message_file:
4204 if options.message:
4205 parser.error('only one of --message and --message-file allowed.')
4206 options.message = gclient_utils.FileRead(options.message_file)
4207 options.message_file = None
4208
tandrii4d0545a2016-07-06 03:56:49 -07004209 if options.cq_dry_run and options.use_commit_queue:
4210 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4211
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004212 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4213 settings.GetIsGerrit()
4214
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004215 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004216 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004217
4218
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004219def IsSubmoduleMergeCommit(ref):
4220 # When submodules are added to the repo, we expect there to be a single
4221 # non-git-svn merge commit at remote HEAD with a signature comment.
4222 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004223 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004224 return RunGit(cmd) != ''
4225
4226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004227def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004228 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004230 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4231 upstream and closes the issue automatically and atomically.
4232
4233 Otherwise (in case of Rietveld):
4234 Squashes branch into a single commit.
4235 Updates changelog with metadata (e.g. pointer to review).
4236 Pushes/dcommits the code upstream.
4237 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004238 """
4239 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4240 help='bypass upload presubmit hook')
4241 parser.add_option('-m', dest='message',
4242 help="override review description")
4243 parser.add_option('-f', action='store_true', dest='force',
4244 help="force yes to questions (don't prompt)")
4245 parser.add_option('-c', dest='contributor',
4246 help="external contributor for patch (appended to " +
4247 "description and used as author for git). Should be " +
4248 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004249 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004250 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004252 auth_config = auth.extract_auth_config_from_options(options)
4253
4254 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004256 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4257 if cl.IsGerrit():
4258 if options.message:
4259 # This could be implemented, but it requires sending a new patch to
4260 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4261 # Besides, Gerrit has the ability to change the commit message on submit
4262 # automatically, thus there is no need to support this option (so far?).
4263 parser.error('-m MESSAGE option is not supported for Gerrit.')
4264 if options.contributor:
4265 parser.error(
4266 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4267 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4268 'the contributor\'s "name <email>". If you can\'t upload such a '
4269 'commit for review, contact your repository admin and request'
4270 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004271 if not cl.GetIssue():
4272 DieWithError('You must upload the issue first to Gerrit.\n'
4273 ' If you would rather have `git cl land` upload '
4274 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004275 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4276 options.verbose)
4277
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004278 current = cl.GetBranch()
4279 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4280 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print()
4282 print('Attempting to push branch %r into another local branch!' % current)
4283 print()
4284 print('Either reparent this branch on top of origin/master:')
4285 print(' git reparent-branch --root')
4286 print()
4287 print('OR run `git rebase-update` if you think the parent branch is ')
4288 print('already committed.')
4289 print()
4290 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004291 return 1
4292
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004293 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004294 # Default to merging against our best guess of the upstream branch.
4295 args = [cl.GetUpstreamBranch()]
4296
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004297 if options.contributor:
4298 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004300 return 1
4301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004302 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004303 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304
sbc@chromium.org71437c02015-04-09 19:29:40 +00004305 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004306 return 1
4307
4308 # This rev-list syntax means "show all commits not in my branch that
4309 # are in base_branch".
4310 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4311 base_branch]).splitlines()
4312 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004313 print('Base branch "%s" has %d commits '
4314 'not in this branch.' % (base_branch, len(upstream_commits)))
4315 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004316 return 1
4317
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004318 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004319 svn_head = None
4320 if cmd == 'dcommit' or base_has_submodules:
4321 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4322 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004323
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004324 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004325 # If the base_head is a submodule merge commit, the first parent of the
4326 # base_head should be a git-svn commit, which is what we're interested in.
4327 base_svn_head = base_branch
4328 if base_has_submodules:
4329 base_svn_head += '^1'
4330
4331 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('This branch has %d additional commits not upstreamed yet.'
4334 % len(extra_commits.splitlines()))
4335 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4336 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337 return 1
4338
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004339 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004340 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004341 author = None
4342 if options.contributor:
4343 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004344 hook_results = cl.RunHook(
4345 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004346 may_prompt=not options.force,
4347 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004348 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004349 if not hook_results.should_continue():
4350 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004352 # Check the tree status if the tree status URL is set.
4353 status = GetTreeStatus()
4354 if 'closed' == status:
4355 print('The tree is closed. Please wait for it to reopen. Use '
4356 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4357 return 1
4358 elif 'unknown' == status:
4359 print('Unable to determine tree status. Please verify manually and '
4360 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4361 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004363 change_desc = ChangeDescription(options.message)
4364 if not change_desc.description and cl.GetIssue():
4365 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004367 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004368 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004369 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004370 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004371 print('No description set.')
4372 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004373 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004375 # Keep a separate copy for the commit message, because the commit message
4376 # contains the link to the Rietveld issue, while the Rietveld message contains
4377 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004378 # Keep a separate copy for the commit message.
4379 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004380 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004381
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004382 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004383 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004384 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004385 # after it. Add a period on a new line to circumvent this. Also add a space
4386 # before the period to make sure that Gitiles continues to correctly resolve
4387 # the URL.
4388 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004390 commit_desc.append_footer('Patch from %s.' % options.contributor)
4391
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004392 print('Description:')
4393 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004395 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004396 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004397 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004399 # We want to squash all this branch's commits into one commit with the proper
4400 # description. We do this by doing a "reset --soft" to the base branch (which
4401 # keeps the working copy the same), then dcommitting that. If origin/master
4402 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4403 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004405 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4406 # Delete the branches if they exist.
4407 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4408 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4409 result = RunGitWithCode(showref_cmd)
4410 if result[0] == 0:
4411 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412
4413 # We might be in a directory that's present in this branch but not in the
4414 # trunk. Move up to the top of the tree so that git commands that expect a
4415 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004416 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004417 if rel_base_path:
4418 os.chdir(rel_base_path)
4419
4420 # Stuff our change into the merge branch.
4421 # We wrap in a try...finally block so if anything goes wrong,
4422 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004423 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004424 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004425 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004426 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004428 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004429 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004431 RunGit(
4432 [
4433 'commit', '--author', options.contributor,
4434 '-m', commit_desc.description,
4435 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004437 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004438 if base_has_submodules:
4439 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4440 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4441 RunGit(['checkout', CHERRY_PICK_BRANCH])
4442 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004443 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004444 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004445 mirror = settings.GetGitMirror(remote)
4446 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004447 pending_prefix = settings.GetPendingRefPrefix()
4448 if not pending_prefix or branch.startswith(pending_prefix):
4449 # If not using refs/pending/heads/* at all, or target ref is already set
4450 # to pending, then push to the target ref directly.
4451 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004452 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004453 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004454 else:
4455 # Cherry-pick the change on top of pending ref and then push it.
4456 assert branch.startswith('refs/'), branch
4457 assert pending_prefix[-1] == '/', pending_prefix
4458 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004459 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004460 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004461 if retcode == 0:
4462 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004463 else:
4464 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004465 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004466 'svn', 'dcommit',
4467 '-C%s' % options.similarity,
4468 '--no-rebase', '--rmdir',
4469 ]
4470 if settings.GetForceHttpsCommitUrl():
4471 # Allow forcing https commit URLs for some projects that don't allow
4472 # committing to http URLs (like Google Code).
4473 remote_url = cl.GetGitSvnRemoteUrl()
4474 if urlparse.urlparse(remote_url).scheme == 'http':
4475 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004476 cmd_args.append('--commit-url=%s' % remote_url)
4477 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004478 if 'Committed r' in output:
4479 revision = re.match(
4480 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4481 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482 finally:
4483 # And then swap back to the original branch and clean up.
4484 RunGit(['checkout', '-q', cl.GetBranch()])
4485 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004486 if base_has_submodules:
4487 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004488
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004489 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004490 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004491 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004492
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004493 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004494 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004495 try:
4496 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4497 # We set pushed_to_pending to False, since it made it all the way to the
4498 # real ref.
4499 pushed_to_pending = False
4500 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004501 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004502
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004503 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004504 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004505 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004506 if not to_pending:
4507 if viewvc_url and revision:
4508 change_desc.append_footer(
4509 'Committed: %s%s' % (viewvc_url, revision))
4510 elif revision:
4511 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004512 print('Closing issue '
4513 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004514 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004515 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004516 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004517 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004518 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004519 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004520 if options.bypass_hooks:
4521 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4522 else:
4523 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004524 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004525
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004526 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004527 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004528 print('The commit is in the pending queue (%s).' % pending_ref)
4529 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4530 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004531
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004532 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4533 if os.path.isfile(hook):
4534 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004535
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004536 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537
4538
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004539def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004540 print()
4541 print('Waiting for commit to be landed on %s...' % real_ref)
4542 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004543 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4544 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004545 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004546
4547 loop = 0
4548 while True:
4549 sys.stdout.write('fetching (%d)... \r' % loop)
4550 sys.stdout.flush()
4551 loop += 1
4552
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004553 if mirror:
4554 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004555 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4556 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4557 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4558 for commit in commits.splitlines():
4559 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004560 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004561 return commit
4562
4563 current_rev = to_rev
4564
4565
tandriibf429402016-09-14 07:09:12 -07004566def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004567 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4568
4569 Returns:
4570 (retcode of last operation, output log of last operation).
4571 """
4572 assert pending_ref.startswith('refs/'), pending_ref
4573 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4574 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4575 code = 0
4576 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004577 max_attempts = 3
4578 attempts_left = max_attempts
4579 while attempts_left:
4580 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004581 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004582 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004583
4584 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004585 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004586 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004587 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004588 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004589 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004590 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004591 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004592 continue
4593
4594 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004595 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004596 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004597 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004598 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004599 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4600 'the following files have merge conflicts:' % pending_ref)
4601 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4602 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004603 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004604 return code, out
4605
4606 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004607 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004608 code, out = RunGitWithCode(
4609 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4610 if code == 0:
4611 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004612 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004613 return code, out
4614
vapiera7fbd5a2016-06-16 09:17:49 -07004615 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004616 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004617 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004618 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004619 print('Fatal push error. Make sure your .netrc credentials and git '
4620 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004621 return code, out
4622
vapiera7fbd5a2016-06-16 09:17:49 -07004623 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004624 return code, out
4625
4626
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004627def IsFatalPushFailure(push_stdout):
4628 """True if retrying push won't help."""
4629 return '(prohibited by Gerrit)' in push_stdout
4630
4631
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004632@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004634 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004635 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004636 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004637 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004638 message = """This repository appears to be a git-svn mirror, but we
4639don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004640 else:
4641 message = """This doesn't appear to be an SVN repository.
4642If your project has a true, writeable git repository, you probably want to run
4643'git cl land' instead.
4644If your project has a git mirror of an upstream SVN master, you probably need
4645to run 'git svn init'.
4646
4647Using the wrong command might cause your commit to appear to succeed, and the
4648review to be closed, without actually landing upstream. If you choose to
4649proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004650 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004651 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004652 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4653 'Please let us know of this project you are committing to:'
4654 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655 return SendUpstream(parser, args, 'dcommit')
4656
4657
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004658@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004659def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004660 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004661 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662 print('This appears to be an SVN repository.')
4663 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004664 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004665 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004666 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004667
4668
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004669@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004670def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004671 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004672 parser.add_option('-b', dest='newbranch',
4673 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004674 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004676 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4677 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004678 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004679 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004680 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004681 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004683 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004684
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004685
4686 group = optparse.OptionGroup(
4687 parser,
4688 'Options for continuing work on the current issue uploaded from a '
4689 'different clone (e.g. different machine). Must be used independently '
4690 'from the other options. No issue number should be specified, and the '
4691 'branch must have an issue number associated with it')
4692 group.add_option('--reapply', action='store_true', dest='reapply',
4693 help='Reset the branch and reapply the issue.\n'
4694 'CAUTION: This will undo any local changes in this '
4695 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004696
4697 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004698 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004699 parser.add_option_group(group)
4700
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004701 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004702 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004703 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004704 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004705 auth_config = auth.extract_auth_config_from_options(options)
4706
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004707
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004708 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004709 if options.newbranch:
4710 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004711 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004712 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004713
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004714 cl = Changelist(auth_config=auth_config,
4715 codereview=options.forced_codereview)
4716 if not cl.GetIssue():
4717 parser.error('current branch must have an associated issue')
4718
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004719 upstream = cl.GetUpstreamBranch()
4720 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004721 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004722
4723 RunGit(['reset', '--hard', upstream])
4724 if options.pull:
4725 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004726
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004727 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4728 options.directory)
4729
4730 if len(args) != 1 or not args[0]:
4731 parser.error('Must specify issue number or url')
4732
4733 # We don't want uncommitted changes mixed up with the patch.
4734 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004735 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004736
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004737 if options.newbranch:
4738 if options.force:
4739 RunGit(['branch', '-D', options.newbranch],
4740 stderr=subprocess2.PIPE, error_ok=True)
4741 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004742 elif not GetCurrentBranch():
4743 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004744
4745 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4746
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004747 if cl.IsGerrit():
4748 if options.reject:
4749 parser.error('--reject is not supported with Gerrit codereview.')
4750 if options.nocommit:
4751 parser.error('--nocommit is not supported with Gerrit codereview.')
4752 if options.directory:
4753 parser.error('--directory is not supported with Gerrit codereview.')
4754
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004755 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004756 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004757
4758
4759def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004760 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004761 # Provide a wrapper for git svn rebase to help avoid accidental
4762 # git svn dcommit.
4763 # It's the only command that doesn't use parser at all since we just defer
4764 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004765
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004766 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004767
4768
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004769def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004770 """Fetches the tree status and returns either 'open', 'closed',
4771 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004772 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004773 if url:
4774 status = urllib2.urlopen(url).read().lower()
4775 if status.find('closed') != -1 or status == '0':
4776 return 'closed'
4777 elif status.find('open') != -1 or status == '1':
4778 return 'open'
4779 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004780 return 'unset'
4781
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783def GetTreeStatusReason():
4784 """Fetches the tree status from a json url and returns the message
4785 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004786 url = settings.GetTreeStatusUrl()
4787 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788 connection = urllib2.urlopen(json_url)
4789 status = json.loads(connection.read())
4790 connection.close()
4791 return status['message']
4792
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004795 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004796 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004797 status = GetTreeStatus()
4798 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004799 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004800 return 2
4801
vapiera7fbd5a2016-06-16 09:17:49 -07004802 print('The tree is %s' % status)
4803 print()
4804 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004805 if status != 'open':
4806 return 1
4807 return 0
4808
4809
maruel@chromium.org15192402012-09-06 12:38:29 +00004810def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004811 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004812 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004813 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004814 '-b', '--bot', action='append',
4815 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4816 'times to specify multiple builders. ex: '
4817 '"-b win_rel -b win_layout". See '
4818 'the try server waterfall for the builders name and the tests '
4819 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004820 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004821 '-B', '--bucket', default='',
4822 help=('Buildbucket bucket to send the try requests.'))
4823 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004824 '-m', '--master', default='',
4825 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004828 help='Revision to use for the try job; default: the revision will '
4829 'be determined by the try recipe that builder runs, which usually '
4830 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004831 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004832 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004833 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004834 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004835 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004836 '--project',
4837 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004838 'in recipe to determine to which repository or directory to '
4839 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004840 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004841 '-p', '--property', dest='properties', action='append', default=[],
4842 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004843 'key2=value2 etc. The value will be treated as '
4844 'json if decodable, or as string otherwise. '
4845 'NOTE: using this may make your try job not usable for CQ, '
4846 'which will then schedule another try job with default properties')
4847 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004848 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004849 '-n', '--name', help='Try job name; default to current branch name')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004850 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004851 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4852 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004853 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004854 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004855 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004856 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004857
machenbach@chromium.org45453142015-09-15 08:45:22 +00004858 # Make sure that all properties are prop=value pairs.
4859 bad_params = [x for x in options.properties if '=' not in x]
4860 if bad_params:
4861 parser.error('Got properties with missing "=": %s' % bad_params)
4862
maruel@chromium.org15192402012-09-06 12:38:29 +00004863 if args:
4864 parser.error('Unknown arguments: %s' % args)
4865
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004866 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004867 if not cl.GetIssue():
4868 parser.error('Need to upload first')
4869
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004870 if cl.IsGerrit():
4871 parser.error(
4872 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4873 'If your project has Commit Queue, dry run is a workaround:\n'
4874 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004875
tandriie113dfd2016-10-11 10:20:12 -07004876 error_message = cl.CannotTriggerTryJobReason()
4877 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004878 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004879
maruel@chromium.org15192402012-09-06 12:38:29 +00004880 if not options.name:
4881 options.name = cl.GetBranch()
4882
borenet6c0efe62016-10-19 08:13:29 -07004883 if options.bucket and options.master:
4884 parser.error('Only one of --bucket and --master may be used.')
4885
qyearsley1fdfcb62016-10-24 13:22:03 -07004886 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004887
qyearsley1fdfcb62016-10-24 13:22:03 -07004888 if not buckets:
4889 # Default to triggering Dry Run (see http://crbug.com/625697).
4890 if options.verbose:
4891 print('git cl try with no bots now defaults to CQ Dry Run.')
4892 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004893
borenet6c0efe62016-10-19 08:13:29 -07004894 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004895 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004896 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004897 'of bot requires an initial job from a parent (usually a builder). '
4898 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004899 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004900 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004901
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004902 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004903 if patchset != cl.GetPatchset():
4904 print('Warning: Codereview server has newer patchsets (%s) than most '
4905 'recent upload from local checkout (%s). Did a previous upload '
4906 'fail?\n'
4907 'By default, git cl try uses the latest patchset from '
4908 'codereview, continuing to use patchset %s.\n' %
4909 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004910
tandrii568043b2016-10-11 07:49:18 -07004911 try:
borenet6c0efe62016-10-19 08:13:29 -07004912 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4913 patchset)
tandrii568043b2016-10-11 07:49:18 -07004914 except BuildbucketResponseException as ex:
4915 print('ERROR: %s' % ex)
4916 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004917 return 0
4918
4919
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004920def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004921 """Prints info about try jobs associated with current CL."""
4922 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004923 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004924 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004925 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004926 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004927 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004928 '--color', action='store_true', default=setup_color.IS_TTY,
4929 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004931 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4932 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004933 group.add_option(
4934 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004935 parser.add_option_group(group)
4936 auth.add_auth_options(parser)
4937 options, args = parser.parse_args(args)
4938 if args:
4939 parser.error('Unrecognized args: %s' % ' '.join(args))
4940
4941 auth_config = auth.extract_auth_config_from_options(options)
4942 cl = Changelist(auth_config=auth_config)
4943 if not cl.GetIssue():
4944 parser.error('Need to upload first')
4945
tandrii221ab252016-10-06 08:12:04 -07004946 patchset = options.patchset
4947 if not patchset:
4948 patchset = cl.GetMostRecentPatchset()
4949 if not patchset:
4950 parser.error('Codereview doesn\'t know about issue %s. '
4951 'No access to issue or wrong issue number?\n'
4952 'Either upload first, or pass --patchset explicitely' %
4953 cl.GetIssue())
4954
4955 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004956 print('Warning: Codereview server has newer patchsets (%s) than most '
4957 'recent upload from local checkout (%s). Did a previous upload '
4958 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004959 'By default, git cl try-results uses the latest patchset from '
4960 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004961 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004962 try:
tandrii221ab252016-10-06 08:12:04 -07004963 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004964 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004965 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004966 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004967 if options.json:
4968 write_try_results_json(options.json, jobs)
4969 else:
4970 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004971 return 0
4972
4973
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004974@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004975def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004976 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004977 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004978 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004979 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004981 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004982 if args:
4983 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004984 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004985 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004986 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004987 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004988
4989 # Clear configured merge-base, if there is one.
4990 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004991 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004992 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004993 return 0
4994
4995
thestig@chromium.org00858c82013-12-02 23:08:03 +00004996def CMDweb(parser, args):
4997 """Opens the current CL in the web browser."""
4998 _, args = parser.parse_args(args)
4999 if args:
5000 parser.error('Unrecognized args: %s' % ' '.join(args))
5001
5002 issue_url = Changelist().GetIssueURL()
5003 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005004 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005005 return 1
5006
5007 webbrowser.open(issue_url)
5008 return 0
5009
5010
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005011def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005012 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005013 parser.add_option('-d', '--dry-run', action='store_true',
5014 help='trigger in dry run mode')
5015 parser.add_option('-c', '--clear', action='store_true',
5016 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005017 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005018 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005019 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005020 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005021 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005022 if args:
5023 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005024 if options.dry_run and options.clear:
5025 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5026
iannuccie53c9352016-08-17 14:40:40 -07005027 cl = Changelist(auth_config=auth_config, issue=options.issue,
5028 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005029 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005030 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005031 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005032 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005033 state = _CQState.DRY_RUN
5034 else:
5035 state = _CQState.COMMIT
5036 if not cl.GetIssue():
5037 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005038 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005039 return 0
5040
5041
groby@chromium.org411034a2013-02-26 15:12:01 +00005042def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005043 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005044 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005045 auth.add_auth_options(parser)
5046 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005047 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005048 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005049 if args:
5050 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005051 cl = Changelist(auth_config=auth_config, issue=options.issue,
5052 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005053 # Ensure there actually is an issue to close.
5054 cl.GetDescription()
5055 cl.CloseIssue()
5056 return 0
5057
5058
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005059def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005060 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005061 parser.add_option(
5062 '--stat',
5063 action='store_true',
5064 dest='stat',
5065 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005066 auth.add_auth_options(parser)
5067 options, args = parser.parse_args(args)
5068 auth_config = auth.extract_auth_config_from_options(options)
5069 if args:
5070 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005071
5072 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005073 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005074 # Staged changes would be committed along with the patch from last
5075 # upload, hence counted toward the "last upload" side in the final
5076 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005077 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005078 return 1
5079
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005080 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005081 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005082 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005083 if not issue:
5084 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005085 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005086 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005087
5088 # Create a new branch based on the merge-base
5089 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005090 # Clear cached branch in cl object, to avoid overwriting original CL branch
5091 # properties.
5092 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005093 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005094 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005095 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005096 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005097 return rtn
5098
wychen@chromium.org06928532015-02-03 02:11:29 +00005099 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005100 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005101 cmd = ['git', 'diff']
5102 if options.stat:
5103 cmd.append('--stat')
5104 cmd.extend([TMP_BRANCH, branch, '--'])
5105 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005106 finally:
5107 RunGit(['checkout', '-q', branch])
5108 RunGit(['branch', '-D', TMP_BRANCH])
5109
5110 return 0
5111
5112
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005113def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005114 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005115 parser.add_option(
5116 '--no-color',
5117 action='store_true',
5118 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005119 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005120 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005121 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005122
5123 author = RunGit(['config', 'user.email']).strip() or None
5124
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005125 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005126
5127 if args:
5128 if len(args) > 1:
5129 parser.error('Unknown args')
5130 base_branch = args[0]
5131 else:
5132 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005133 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005134
5135 change = cl.GetChange(base_branch, None)
5136 return owners_finder.OwnersFinder(
5137 [f.LocalPath() for f in
5138 cl.GetChange(base_branch, None).AffectedFiles()],
5139 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005140 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005141 disable_color=options.no_color).run()
5142
5143
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005144def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005145 """Generates a diff command."""
5146 # Generate diff for the current branch's changes.
5147 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5148 upstream_commit, '--' ]
5149
5150 if args:
5151 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005152 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005153 diff_cmd.append(arg)
5154 else:
5155 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005156
5157 return diff_cmd
5158
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005159def MatchingFileType(file_name, extensions):
5160 """Returns true if the file name ends with one of the given extensions."""
5161 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005162
enne@chromium.org555cfe42014-01-29 18:21:39 +00005163@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005164def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005165 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005166 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005167 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005168 parser.add_option('--full', action='store_true',
5169 help='Reformat the full content of all touched files')
5170 parser.add_option('--dry-run', action='store_true',
5171 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005172 parser.add_option('--python', action='store_true',
5173 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005174 parser.add_option('--diff', action='store_true',
5175 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005176 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005177
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005178 # git diff generates paths against the root of the repository. Change
5179 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005180 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005181 if rel_base_path:
5182 os.chdir(rel_base_path)
5183
digit@chromium.org29e47272013-05-17 17:01:46 +00005184 # Grab the merge-base commit, i.e. the upstream commit of the current
5185 # branch when it was created or the last time it was rebased. This is
5186 # to cover the case where the user may have called "git fetch origin",
5187 # moving the origin branch to a newer commit, but hasn't rebased yet.
5188 upstream_commit = None
5189 cl = Changelist()
5190 upstream_branch = cl.GetUpstreamBranch()
5191 if upstream_branch:
5192 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5193 upstream_commit = upstream_commit.strip()
5194
5195 if not upstream_commit:
5196 DieWithError('Could not find base commit for this branch. '
5197 'Are you in detached state?')
5198
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005199 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5200 diff_output = RunGit(changed_files_cmd)
5201 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005202 # Filter out files deleted by this CL
5203 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005204
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005205 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5206 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5207 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005208 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005209
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005210 top_dir = os.path.normpath(
5211 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5212
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005213 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5214 # formatted. This is used to block during the presubmit.
5215 return_value = 0
5216
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005217 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005218 # Locate the clang-format binary in the checkout
5219 try:
5220 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005221 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005222 DieWithError(e)
5223
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005224 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005225 cmd = [clang_format_tool]
5226 if not opts.dry_run and not opts.diff:
5227 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005228 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005229 if opts.diff:
5230 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005231 else:
5232 env = os.environ.copy()
5233 env['PATH'] = str(os.path.dirname(clang_format_tool))
5234 try:
5235 script = clang_format.FindClangFormatScriptInChromiumTree(
5236 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005237 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005238 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005239
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005240 cmd = [sys.executable, script, '-p0']
5241 if not opts.dry_run and not opts.diff:
5242 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005243
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005244 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5245 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005246
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005247 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5248 if opts.diff:
5249 sys.stdout.write(stdout)
5250 if opts.dry_run and len(stdout) > 0:
5251 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005252
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005253 # Similar code to above, but using yapf on .py files rather than clang-format
5254 # on C/C++ files
5255 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005256 yapf_tool = gclient_utils.FindExecutable('yapf')
5257 if yapf_tool is None:
5258 DieWithError('yapf not found in PATH')
5259
5260 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005261 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005262 cmd = [yapf_tool]
5263 if not opts.dry_run and not opts.diff:
5264 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005265 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005266 if opts.diff:
5267 sys.stdout.write(stdout)
5268 else:
5269 # TODO(sbc): yapf --lines mode still has some issues.
5270 # https://github.com/google/yapf/issues/154
5271 DieWithError('--python currently only works with --full')
5272
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005273 # Dart's formatter does not have the nice property of only operating on
5274 # modified chunks, so hard code full.
5275 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005276 try:
5277 command = [dart_format.FindDartFmtToolInChromiumTree()]
5278 if not opts.dry_run and not opts.diff:
5279 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005280 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005281
ppi@chromium.org6593d932016-03-03 15:41:15 +00005282 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005283 if opts.dry_run and stdout:
5284 return_value = 2
5285 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005286 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5287 'found in this checkout. Files in other languages are still '
5288 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005289
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005290 # Format GN build files. Always run on full build files for canonical form.
5291 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005292 cmd = ['gn', 'format' ]
5293 if opts.dry_run or opts.diff:
5294 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005295 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005296 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5297 shell=sys.platform == 'win32',
5298 cwd=top_dir)
5299 if opts.dry_run and gn_ret == 2:
5300 return_value = 2 # Not formatted.
5301 elif opts.diff and gn_ret == 2:
5302 # TODO this should compute and print the actual diff.
5303 print("This change has GN build file diff for " + gn_diff_file)
5304 elif gn_ret != 0:
5305 # For non-dry run cases (and non-2 return values for dry-run), a
5306 # nonzero error code indicates a failure, probably because the file
5307 # doesn't parse.
5308 DieWithError("gn format failed on " + gn_diff_file +
5309 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005310
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005311 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005312
5313
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005314@subcommand.usage('<codereview url or issue id>')
5315def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005316 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005317 _, args = parser.parse_args(args)
5318
5319 if len(args) != 1:
5320 parser.print_help()
5321 return 1
5322
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005323 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005324 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005325 parser.print_help()
5326 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005327 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005328
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005329 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005330 output = RunGit(['config', '--local', '--get-regexp',
5331 r'branch\..*\.%s' % issueprefix],
5332 error_ok=True)
5333 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005334 if issue == target_issue:
5335 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005336
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005337 branches = []
5338 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005339 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005340 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005341 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005342 return 1
5343 if len(branches) == 1:
5344 RunGit(['checkout', branches[0]])
5345 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005346 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005347 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005348 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005349 which = raw_input('Choose by index: ')
5350 try:
5351 RunGit(['checkout', branches[int(which)]])
5352 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005353 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005354 return 1
5355
5356 return 0
5357
5358
maruel@chromium.org29404b52014-09-08 22:58:00 +00005359def CMDlol(parser, args):
5360 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005362 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5363 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5364 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005365 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005366 return 0
5367
5368
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005369class OptionParser(optparse.OptionParser):
5370 """Creates the option parse and add --verbose support."""
5371 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005372 optparse.OptionParser.__init__(
5373 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005374 self.add_option(
5375 '-v', '--verbose', action='count', default=0,
5376 help='Use 2 times for more debugging info')
5377
5378 def parse_args(self, args=None, values=None):
5379 options, args = optparse.OptionParser.parse_args(self, args, values)
5380 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5381 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5382 return options, args
5383
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005385def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005386 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005387 print('\nYour python version %s is unsupported, please upgrade.\n' %
5388 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005389 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005390
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005391 # Reload settings.
5392 global settings
5393 settings = Settings()
5394
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005395 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005396 dispatcher = subcommand.CommandDispatcher(__name__)
5397 try:
5398 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005399 except auth.AuthenticationError as e:
5400 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005401 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005402 if e.code != 500:
5403 raise
5404 DieWithError(
5405 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5406 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005407 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005408
5409
5410if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005411 # These affect sys.stdout so do it outside of main() to simplify mocks in
5412 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005413 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005414 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005415 try:
5416 sys.exit(main(sys.argv[1:]))
5417 except KeyboardInterrupt:
5418 sys.stderr.write('interrupted\n')
5419 sys.exit(1)