blob: 6b0d304913564e07865e76fdb768534aa41c36df [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.
qyearsley123a4682016-10-26 09:12:17 -0700366 # TODO(qyearsley): Remove this.
qyearsley1fdfcb62016-10-24 13:22:03 -0700367 options.bot = presubmit_support.DoGetTrySlaves(
368 change=change,
369 changed_files=change.LocalPaths(),
370 repository_root=settings.GetRoot(),
371 default_presubmit=None,
372 project=None,
373 verbose=options.verbose,
374 output_stream=sys.stdout)
375
376 if not options.bot:
377 return {}
378
379 if options.bucket:
380 return {options.bucket: {b: [] for b in options.bot}}
381
qyearsley123a4682016-10-26 09:12:17 -0700382 if not options.master:
383 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
384 if error_message:
385 option_parser.error(
386 'Tryserver master cannot be found because: %s\n'
387 'Please manually specify the tryserver master, e.g. '
388 '"-m tryserver.chromium.linux".' % error_message)
389 return bucket_map
390
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 builders_and_tests = {}
392
393 # TODO(machenbach): The old style command-line options don't support
394 # multiple try masters yet.
qyearsley123a4682016-10-26 09:12:17 -0700395 # TODO(qyearsley): If options.bot is always a list of strings, then
396 # "new_style" never applies, and so we should remove support for Specifying
397 # test filters completely.
qyearsley1fdfcb62016-10-24 13:22:03 -0700398 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
399 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
400
401 for bot in old_style:
402 if ':' in bot:
403 option_parser.error('Specifying testfilter is no longer supported')
404 elif ',' in bot:
405 option_parser.error('Specify one bot per --bot flag')
406 else:
407 builders_and_tests.setdefault(bot, [])
408
409 for bot, tests in new_style:
410 builders_and_tests.setdefault(bot, []).extend(tests)
411
qyearsley123a4682016-10-26 09:12:17 -0700412 # Add the "master." prefix to the master name to obtain the bucket name.
413 bucket = _prefix_master(options.master)
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 return {bucket: builders_and_tests}
415
416
qyearsley123a4682016-10-26 09:12:17 -0700417def _get_bucket_map_for_builders(builders):
418 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 map_url = 'https://builders-map.appspot.com/'
420 try:
qyearsley123a4682016-10-26 09:12:17 -0700421 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 except urllib2.URLError as e:
423 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
424 (map_url, e))
425 except ValueError as e:
426 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700427 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 return None, 'Failed to build master map.'
429
qyearsley123a4682016-10-26 09:12:17 -0700430 bucket_map = {}
431 for builder in builders:
432 builder = builder.split(':', 1)[0]
433 masters = builders_map.get(builder, [])
434 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700436 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700438 (builder, masters))
439 bucket = _prefix_master(masters[0])
440 bucket_map.setdefault(bucket, {})[builder] = []
441
442 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
borenet6c0efe62016-10-19 08:13:29 -0700445def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700446 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 """Sends a request to Buildbucket to trigger try jobs for a changelist.
448
449 Args:
450 auth_config: AuthConfig for Rietveld.
451 changelist: Changelist that the try jobs are associated with.
452 buckets: A nested dict mapping bucket names to builders to tests.
453 options: Command-line options.
454 """
tandriide281ae2016-10-12 06:02:30 -0700455 assert changelist.GetIssue(), 'CL must be uploaded first'
456 codereview_url = changelist.GetCodereviewServer()
457 assert codereview_url, 'CL must be uploaded first'
458 patchset = patchset or changelist.GetMostRecentPatchset()
459 assert patchset, 'CL must be uploaded first'
460
461 codereview_host = urlparse.urlparse(codereview_url).hostname
462 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 http = authenticator.authorize(httplib2.Http())
464 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700465
466 # TODO(tandrii): consider caching Gerrit CL details just like
467 # _RietveldChangelistImpl does, then caching values in these two variables
468 # won't be necessary.
469 owner_email = changelist.GetIssueOwner()
470 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000471
472 buildbucket_put_url = (
473 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000474 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700475 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
476 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
477 hostname=codereview_host,
478 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000479 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700480 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000481
482 batch_req_body = {'builds': []}
483 print_text = []
484 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700485 for bucket, builders_and_tests in sorted(buckets.iteritems()):
486 print_text.append('Bucket: %s' % bucket)
487 master = None
488 if bucket.startswith(MASTER_PREFIX):
489 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 for builder, tests in sorted(builders_and_tests.iteritems()):
491 print_text.append(' %s: %s' % (builder, tests))
492 parameters = {
493 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000494 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700495 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000496 'revision': options.revision,
497 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 'properties': {
499 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700500 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700501 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000502 'patch_storage': 'rietveld',
503 'patchset': patchset,
tandriide281ae2016-10-12 06:02:30 -0700504 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000505 },
506 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000507 if 'presubmit' in builder.lower():
508 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000509 if tests:
510 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700511 if extra_properties:
512 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 if options.clobber:
514 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700515
516 tags = [
517 'builder:%s' % builder,
518 'buildset:%s' % buildset,
519 'user_agent:git_cl_try',
520 ]
521 if master:
522 parameters['properties']['master'] = master
523 tags.append('master:%s' % master)
524
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525 batch_req_body['builds'].append(
526 {
527 'bucket': bucket,
528 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700530 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000531 }
532 )
533
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700535 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536 http,
537 buildbucket_put_url,
538 'PUT',
539 body=json.dumps(batch_req_body),
540 headers={'Content-Type': 'application/json'}
541 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000542 print_text.append('To see results here, run: git cl try-results')
543 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700544 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000545
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000546
tandrii221ab252016-10-06 08:12:04 -0700547def fetch_try_jobs(auth_config, changelist, buildbucket_host,
548 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700549 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550
qyearsley53f48a12016-09-01 10:45:13 -0700551 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 """
tandrii221ab252016-10-06 08:12:04 -0700553 assert buildbucket_host
554 assert changelist.GetIssue(), 'CL must be uploaded first'
555 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
556 patchset = patchset or changelist.GetMostRecentPatchset()
557 assert patchset, 'CL must be uploaded first'
558
559 codereview_url = changelist.GetCodereviewServer()
560 codereview_host = urlparse.urlparse(codereview_url).hostname
561 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 if authenticator.has_cached_credentials():
563 http = authenticator.authorize(httplib2.Http())
564 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700565 print('Warning: Some results might be missing because %s' %
566 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700567 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 http = httplib2.Http()
569
570 http.force_exception_to_status_code = True
571
tandrii221ab252016-10-06 08:12:04 -0700572 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
573 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
574 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700576 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 params = {'tag': 'buildset:%s' % buildset}
578
579 builds = {}
580 while True:
581 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700582 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700584 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 for build in content.get('builds', []):
586 builds[build['id']] = build
587 if 'next_cursor' in content:
588 params['start_cursor'] = content['next_cursor']
589 else:
590 break
591 return builds
592
593
qyearsleyeab3c042016-08-24 09:18:28 -0700594def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 """Prints nicely result of fetch_try_jobs."""
596 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700597 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 return
599
600 # Make a copy, because we'll be modifying builds dictionary.
601 builds = builds.copy()
602 builder_names_cache = {}
603
604 def get_builder(b):
605 try:
606 return builder_names_cache[b['id']]
607 except KeyError:
608 try:
609 parameters = json.loads(b['parameters_json'])
610 name = parameters['builder_name']
611 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700612 print('WARNING: failed to get builder name for build %s: %s' % (
613 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614 name = None
615 builder_names_cache[b['id']] = name
616 return name
617
618 def get_bucket(b):
619 bucket = b['bucket']
620 if bucket.startswith('master.'):
621 return bucket[len('master.'):]
622 return bucket
623
624 if options.print_master:
625 name_fmt = '%%-%ds %%-%ds' % (
626 max(len(str(get_bucket(b))) for b in builds.itervalues()),
627 max(len(str(get_builder(b))) for b in builds.itervalues()))
628 def get_name(b):
629 return name_fmt % (get_bucket(b), get_builder(b))
630 else:
631 name_fmt = '%%-%ds' % (
632 max(len(str(get_builder(b))) for b in builds.itervalues()))
633 def get_name(b):
634 return name_fmt % get_builder(b)
635
636 def sort_key(b):
637 return b['status'], b.get('result'), get_name(b), b.get('url')
638
639 def pop(title, f, color=None, **kwargs):
640 """Pop matching builds from `builds` dict and print them."""
641
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000642 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000643 colorize = str
644 else:
645 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
646
647 result = []
648 for b in builds.values():
649 if all(b.get(k) == v for k, v in kwargs.iteritems()):
650 builds.pop(b['id'])
651 result.append(b)
652 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700653 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000654 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700655 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000656
657 total = len(builds)
658 pop(status='COMPLETED', result='SUCCESS',
659 title='Successes:', color=Fore.GREEN,
660 f=lambda b: (get_name(b), b.get('url')))
661 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
662 title='Infra Failures:', color=Fore.MAGENTA,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
665 title='Failures:', color=Fore.RED,
666 f=lambda b: (get_name(b), b.get('url')))
667 pop(status='COMPLETED', result='CANCELED',
668 title='Canceled:', color=Fore.MAGENTA,
669 f=lambda b: (get_name(b),))
670 pop(status='COMPLETED', result='FAILURE',
671 failure_reason='INVALID_BUILD_DEFINITION',
672 title='Wrong master/builder name:', color=Fore.MAGENTA,
673 f=lambda b: (get_name(b),))
674 pop(status='COMPLETED', result='FAILURE',
675 title='Other failures:',
676 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
677 pop(status='COMPLETED',
678 title='Other finished:',
679 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
680 pop(status='STARTED',
681 title='Started:', color=Fore.YELLOW,
682 f=lambda b: (get_name(b), b.get('url')))
683 pop(status='SCHEDULED',
684 title='Scheduled:',
685 f=lambda b: (get_name(b), 'id=%s' % b['id']))
686 # The last section is just in case buildbucket API changes OR there is a bug.
687 pop(title='Other:',
688 f=lambda b: (get_name(b), 'id=%s' % b['id']))
689 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700690 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000691
692
qyearsley53f48a12016-09-01 10:45:13 -0700693def write_try_results_json(output_file, builds):
694 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
695
696 The input |builds| dict is assumed to be generated by Buildbucket.
697 Buildbucket documentation: http://goo.gl/G0s101
698 """
699
700 def convert_build_dict(build):
701 return {
702 'buildbucket_id': build.get('id'),
703 'status': build.get('status'),
704 'result': build.get('result'),
705 'bucket': build.get('bucket'),
706 'builder_name': json.loads(
707 build.get('parameters_json', '{}')).get('builder_name'),
708 'failure_reason': build.get('failure_reason'),
709 'url': build.get('url'),
710 }
711
712 converted = []
713 for _, build in sorted(builds.items()):
714 converted.append(convert_build_dict(build))
715 write_json(output_file, converted)
716
717
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000718def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
719 """Return the corresponding git ref if |base_url| together with |glob_spec|
720 matches the full |url|.
721
722 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
723 """
724 fetch_suburl, as_ref = glob_spec.split(':')
725 if allow_wildcards:
726 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
727 if glob_match:
728 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
729 # "branches/{472,597,648}/src:refs/remotes/svn/*".
730 branch_re = re.escape(base_url)
731 if glob_match.group(1):
732 branch_re += '/' + re.escape(glob_match.group(1))
733 wildcard = glob_match.group(2)
734 if wildcard == '*':
735 branch_re += '([^/]*)'
736 else:
737 # Escape and replace surrounding braces with parentheses and commas
738 # with pipe symbols.
739 wildcard = re.escape(wildcard)
740 wildcard = re.sub('^\\\\{', '(', wildcard)
741 wildcard = re.sub('\\\\,', '|', wildcard)
742 wildcard = re.sub('\\\\}$', ')', wildcard)
743 branch_re += wildcard
744 if glob_match.group(3):
745 branch_re += re.escape(glob_match.group(3))
746 match = re.match(branch_re, url)
747 if match:
748 return re.sub('\*$', match.group(1), as_ref)
749
750 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
751 if fetch_suburl:
752 full_url = base_url + '/' + fetch_suburl
753 else:
754 full_url = base_url
755 if full_url == url:
756 return as_ref
757 return None
758
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000759
iannucci@chromium.org79540052012-10-19 23:15:26 +0000760def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000761 """Prints statistics about the change to the user."""
762 # --no-ext-diff is broken in some versions of Git, so try to work around
763 # this by overriding the environment (but there is still a problem if the
764 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000766 if 'GIT_EXTERNAL_DIFF' in env:
767 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000768
769 if find_copies:
770 similarity_options = ['--find-copies-harder', '-l100000',
771 '-C%s' % similarity]
772 else:
773 similarity_options = ['-M%s' % similarity]
774
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000775 try:
776 stdout = sys.stdout.fileno()
777 except AttributeError:
778 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000779 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000780 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000781 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000782 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000783
784
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000785class BuildbucketResponseException(Exception):
786 pass
787
788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789class Settings(object):
790 def __init__(self):
791 self.default_server = None
792 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000793 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 self.is_git_svn = None
795 self.svn_branch = None
796 self.tree_status_url = None
797 self.viewvc_url = None
798 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000799 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000800 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000801 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000802 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000803 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000804 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000805 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700806 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807
808 def LazyUpdateIfNeeded(self):
809 """Updates the settings from a codereview.settings file, if available."""
810 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000811 # The only value that actually changes the behavior is
812 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000813 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000814 error_ok=True
815 ).strip().lower()
816
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000818 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 LoadCodereviewSettingsFromFile(cr_settings_file)
820 self.updated = True
821
822 def GetDefaultServerUrl(self, error_ok=False):
823 if not self.default_server:
824 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000825 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000826 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 if error_ok:
828 return self.default_server
829 if not self.default_server:
830 error_message = ('Could not find settings file. You must configure '
831 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000832 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 return self.default_server
835
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000836 @staticmethod
837 def GetRelativeRoot():
838 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000841 if self.root is None:
842 self.root = os.path.abspath(self.GetRelativeRoot())
843 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000845 def GetGitMirror(self, remote='origin'):
846 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000847 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000848 if not os.path.isdir(local_url):
849 return None
850 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
851 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
852 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
853 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
854 if mirror.exists():
855 return mirror
856 return None
857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 def GetIsGitSvn(self):
859 """Return true if this repo looks like it's using git-svn."""
860 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000861 if self.GetPendingRefPrefix():
862 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
863 self.is_git_svn = False
864 else:
865 # If you have any "svn-remote.*" config keys, we think you're using svn.
866 self.is_git_svn = RunGitWithCode(
867 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868 return self.is_git_svn
869
870 def GetSVNBranch(self):
871 if self.svn_branch is None:
872 if not self.GetIsGitSvn():
873 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
874
875 # Try to figure out which remote branch we're based on.
876 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000877 # 1) iterate through our branch history and find the svn URL.
878 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879
880 # regexp matching the git-svn line that contains the URL.
881 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
882
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000883 # We don't want to go through all of history, so read a line from the
884 # pipe at a time.
885 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000886 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000887 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
888 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000889 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000890 for line in proc.stdout:
891 match = git_svn_re.match(line)
892 if match:
893 url = match.group(1)
894 proc.stdout.close() # Cut pipe.
895 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000897 if url:
898 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
899 remotes = RunGit(['config', '--get-regexp',
900 r'^svn-remote\..*\.url']).splitlines()
901 for remote in remotes:
902 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000904 remote = match.group(1)
905 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000906 rewrite_root = RunGit(
907 ['config', 'svn-remote.%s.rewriteRoot' % remote],
908 error_ok=True).strip()
909 if rewrite_root:
910 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000911 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000912 ['config', 'svn-remote.%s.fetch' % remote],
913 error_ok=True).strip()
914 if fetch_spec:
915 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
916 if self.svn_branch:
917 break
918 branch_spec = RunGit(
919 ['config', 'svn-remote.%s.branches' % remote],
920 error_ok=True).strip()
921 if branch_spec:
922 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
923 if self.svn_branch:
924 break
925 tag_spec = RunGit(
926 ['config', 'svn-remote.%s.tags' % remote],
927 error_ok=True).strip()
928 if tag_spec:
929 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
930 if self.svn_branch:
931 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933 if not self.svn_branch:
934 DieWithError('Can\'t guess svn branch -- try specifying it on the '
935 'command line')
936
937 return self.svn_branch
938
939 def GetTreeStatusUrl(self, error_ok=False):
940 if not self.tree_status_url:
941 error_message = ('You must configure your tree status URL by running '
942 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000943 self.tree_status_url = self._GetRietveldConfig(
944 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945 return self.tree_status_url
946
947 def GetViewVCUrl(self):
948 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000949 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 return self.viewvc_url
951
rmistry@google.com90752582014-01-14 21:04:50 +0000952 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000953 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000954
rmistry@google.com78948ed2015-07-08 23:09:57 +0000955 def GetIsSkipDependencyUpload(self, branch_name):
956 """Returns true if specified branch should skip dep uploads."""
957 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
958 error_ok=True)
959
rmistry@google.com5626a922015-02-26 14:03:30 +0000960 def GetRunPostUploadHook(self):
961 run_post_upload_hook = self._GetRietveldConfig(
962 'run-post-upload-hook', error_ok=True)
963 return run_post_upload_hook == "True"
964
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000965 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000966 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000967
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000968 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000969 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000970
ukai@chromium.orge8077812012-02-03 03:41:46 +0000971 def GetIsGerrit(self):
972 """Return true if this repo is assosiated with gerrit code review system."""
973 if self.is_gerrit is None:
974 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
975 return self.is_gerrit
976
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000977 def GetSquashGerritUploads(self):
978 """Return true if uploads to Gerrit should be squashed by default."""
979 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700980 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
981 if self.squash_gerrit_uploads is None:
982 # Default is squash now (http://crbug.com/611892#c23).
983 self.squash_gerrit_uploads = not (
984 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
985 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000986 return self.squash_gerrit_uploads
987
tandriia60502f2016-06-20 02:01:53 -0700988 def GetSquashGerritUploadsOverride(self):
989 """Return True or False if codereview.settings should be overridden.
990
991 Returns None if no override has been defined.
992 """
993 # See also http://crbug.com/611892#c23
994 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
995 error_ok=True).strip()
996 if result == 'true':
997 return True
998 if result == 'false':
999 return False
1000 return None
1001
tandrii@chromium.org28253532016-04-14 13:46:56 +00001002 def GetGerritSkipEnsureAuthenticated(self):
1003 """Return True if EnsureAuthenticated should not be done for Gerrit
1004 uploads."""
1005 if self.gerrit_skip_ensure_authenticated is None:
1006 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00001007 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +00001008 error_ok=True).strip() == 'true')
1009 return self.gerrit_skip_ensure_authenticated
1010
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001011 def GetGitEditor(self):
1012 """Return the editor specified in the git config, or None if none is."""
1013 if self.git_editor is None:
1014 self.git_editor = self._GetConfig('core.editor', error_ok=True)
1015 return self.git_editor or None
1016
thestig@chromium.org44202a22014-03-11 19:22:18 +00001017 def GetLintRegex(self):
1018 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
1019 DEFAULT_LINT_REGEX)
1020
1021 def GetLintIgnoreRegex(self):
1022 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
1023 DEFAULT_LINT_IGNORE_REGEX)
1024
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001025 def GetProject(self):
1026 if not self.project:
1027 self.project = self._GetRietveldConfig('project', error_ok=True)
1028 return self.project
1029
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001030 def GetForceHttpsCommitUrl(self):
1031 if not self.force_https_commit_url:
1032 self.force_https_commit_url = self._GetRietveldConfig(
1033 'force-https-commit-url', error_ok=True)
1034 return self.force_https_commit_url
1035
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001036 def GetPendingRefPrefix(self):
1037 if not self.pending_ref_prefix:
1038 self.pending_ref_prefix = self._GetRietveldConfig(
1039 'pending-ref-prefix', error_ok=True)
1040 return self.pending_ref_prefix
1041
tandriif46c20f2016-09-14 06:17:05 -07001042 def GetHasGitNumberFooter(self):
1043 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1044 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1045 if not self.git_number_footer:
1046 self.git_number_footer = self._GetRietveldConfig(
1047 'git-number-footer', error_ok=True)
1048 return self.git_number_footer
1049
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001050 def _GetRietveldConfig(self, param, **kwargs):
1051 return self._GetConfig('rietveld.' + param, **kwargs)
1052
rmistry@google.com78948ed2015-07-08 23:09:57 +00001053 def _GetBranchConfig(self, branch_name, param, **kwargs):
1054 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 def _GetConfig(self, param, **kwargs):
1057 self.LazyUpdateIfNeeded()
1058 return RunGit(['config', param], **kwargs).strip()
1059
1060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061def ShortBranchName(branch):
1062 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001063 return branch.replace('refs/heads/', '', 1)
1064
1065
1066def GetCurrentBranchRef():
1067 """Returns branch ref (e.g., refs/heads/master) or None."""
1068 return RunGit(['symbolic-ref', 'HEAD'],
1069 stderr=subprocess2.VOID, error_ok=True).strip() or None
1070
1071
1072def GetCurrentBranch():
1073 """Returns current branch or None.
1074
1075 For refs/heads/* branches, returns just last part. For others, full ref.
1076 """
1077 branchref = GetCurrentBranchRef()
1078 if branchref:
1079 return ShortBranchName(branchref)
1080 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001083class _CQState(object):
1084 """Enum for states of CL with respect to Commit Queue."""
1085 NONE = 'none'
1086 DRY_RUN = 'dry_run'
1087 COMMIT = 'commit'
1088
1089 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1090
1091
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001092class _ParsedIssueNumberArgument(object):
1093 def __init__(self, issue=None, patchset=None, hostname=None):
1094 self.issue = issue
1095 self.patchset = patchset
1096 self.hostname = hostname
1097
1098 @property
1099 def valid(self):
1100 return self.issue is not None
1101
1102
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001103def ParseIssueNumberArgument(arg):
1104 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1105 fail_result = _ParsedIssueNumberArgument()
1106
1107 if arg.isdigit():
1108 return _ParsedIssueNumberArgument(issue=int(arg))
1109 if not arg.startswith('http'):
1110 return fail_result
1111 url = gclient_utils.UpgradeToHttps(arg)
1112 try:
1113 parsed_url = urlparse.urlparse(url)
1114 except ValueError:
1115 return fail_result
1116 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1117 tmp = cls.ParseIssueURL(parsed_url)
1118 if tmp is not None:
1119 return tmp
1120 return fail_result
1121
1122
tandriic2405f52016-10-10 08:13:15 -07001123class GerritIssueNotExists(Exception):
1124 def __init__(self, issue, url):
1125 self.issue = issue
1126 self.url = url
1127 super(GerritIssueNotExists, self).__init__()
1128
1129 def __str__(self):
1130 return 'issue %s at %s does not exist or you have no access to it' % (
1131 self.issue, self.url)
1132
1133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001135 """Changelist works with one changelist in local branch.
1136
1137 Supports two codereview backends: Rietveld or Gerrit, selected at object
1138 creation.
1139
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001140 Notes:
1141 * Not safe for concurrent multi-{thread,process} use.
1142 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001143 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 """
1145
1146 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1147 """Create a new ChangeList instance.
1148
1149 If issue is given, the codereview must be given too.
1150
1151 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1152 Otherwise, it's decided based on current configuration of the local branch,
1153 with default being 'rietveld' for backwards compatibility.
1154 See _load_codereview_impl for more details.
1155
1156 **kwargs will be passed directly to codereview implementation.
1157 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001159 global settings
1160 if not settings:
1161 # Happens when git_cl.py is used as a utility library.
1162 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001163
1164 if issue:
1165 assert codereview, 'codereview must be known, if issue is known'
1166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 self.branchref = branchref
1168 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001169 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 self.branch = ShortBranchName(self.branchref)
1171 else:
1172 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001174 self.lookedup_issue = False
1175 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 self.has_description = False
1177 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001178 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001180 self.cc = None
1181 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001182 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001183
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001185 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001186 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001187 assert self._codereview_impl
1188 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001189
1190 def _load_codereview_impl(self, codereview=None, **kwargs):
1191 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001192 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1193 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1194 self._codereview = codereview
1195 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001196 return
1197
1198 # Automatic selection based on issue number set for a current branch.
1199 # Rietveld takes precedence over Gerrit.
1200 assert not self.issue
1201 # Whether we find issue or not, we are doing the lookup.
1202 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001203 if self.GetBranch():
1204 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1205 issue = _git_get_branch_config_value(
1206 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1207 if issue:
1208 self._codereview = codereview
1209 self._codereview_impl = cls(self, **kwargs)
1210 self.issue = int(issue)
1211 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212
1213 # No issue is set for this branch, so decide based on repo-wide settings.
1214 return self._load_codereview_impl(
1215 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1216 **kwargs)
1217
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001218 def IsGerrit(self):
1219 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001220
1221 def GetCCList(self):
1222 """Return the users cc'd on this CL.
1223
agable92bec4f2016-08-24 09:27:27 -07001224 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001225 """
1226 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001227 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001228 more_cc = ','.join(self.watchers)
1229 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1230 return self.cc
1231
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001232 def GetCCListWithoutDefault(self):
1233 """Return the users cc'd on this CL excluding default ones."""
1234 if self.cc is None:
1235 self.cc = ','.join(self.watchers)
1236 return self.cc
1237
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001238 def SetWatchers(self, watchers):
1239 """Set the list of email addresses that should be cc'd based on the changed
1240 files in this CL.
1241 """
1242 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243
1244 def GetBranch(self):
1245 """Returns the short branch name, e.g. 'master'."""
1246 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001247 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001248 if not branchref:
1249 return None
1250 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 self.branch = ShortBranchName(self.branchref)
1252 return self.branch
1253
1254 def GetBranchRef(self):
1255 """Returns the full branch name, e.g. 'refs/heads/master'."""
1256 self.GetBranch() # Poke the lazy loader.
1257 return self.branchref
1258
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001259 def ClearBranch(self):
1260 """Clears cached branch data of this object."""
1261 self.branch = self.branchref = None
1262
tandrii5d48c322016-08-18 16:19:37 -07001263 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1264 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1265 kwargs['branch'] = self.GetBranch()
1266 return _git_get_branch_config_value(key, default, **kwargs)
1267
1268 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1269 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1270 assert self.GetBranch(), (
1271 'this CL must have an associated branch to %sset %s%s' %
1272 ('un' if value is None else '',
1273 key,
1274 '' if value is None else ' to %r' % value))
1275 kwargs['branch'] = self.GetBranch()
1276 return _git_set_branch_config_value(key, value, **kwargs)
1277
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001278 @staticmethod
1279 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001280 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281 e.g. 'origin', 'refs/heads/master'
1282 """
1283 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001284 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001287 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001289 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1290 error_ok=True).strip()
1291 if upstream_branch:
1292 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001294 # Fall back on trying a git-svn upstream branch.
1295 if settings.GetIsGitSvn():
1296 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001298 # Else, try to guess the origin remote.
1299 remote_branches = RunGit(['branch', '-r']).split()
1300 if 'origin/master' in remote_branches:
1301 # Fall back on origin/master if it exits.
1302 remote = 'origin'
1303 upstream_branch = 'refs/heads/master'
1304 elif 'origin/trunk' in remote_branches:
1305 # Fall back on origin/trunk if it exists. Generally a shared
1306 # git-svn clone
1307 remote = 'origin'
1308 upstream_branch = 'refs/heads/trunk'
1309 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310 DieWithError(
1311 'Unable to determine default branch to diff against.\n'
1312 'Either pass complete "git diff"-style arguments, like\n'
1313 ' git cl upload origin/master\n'
1314 'or verify this branch is set up to track another \n'
1315 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316
1317 return remote, upstream_branch
1318
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001319 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001320 upstream_branch = self.GetUpstreamBranch()
1321 if not BranchExists(upstream_branch):
1322 DieWithError('The upstream for the current branch (%s) does not exist '
1323 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001324 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001325 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001326
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 def GetUpstreamBranch(self):
1328 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001331 upstream_branch = upstream_branch.replace('refs/heads/',
1332 'refs/remotes/%s/' % remote)
1333 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1334 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 self.upstream_branch = upstream_branch
1336 return self.upstream_branch
1337
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001338 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001339 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 remote, branch = None, self.GetBranch()
1341 seen_branches = set()
1342 while branch not in seen_branches:
1343 seen_branches.add(branch)
1344 remote, branch = self.FetchUpstreamTuple(branch)
1345 branch = ShortBranchName(branch)
1346 if remote != '.' or branch.startswith('refs/remotes'):
1347 break
1348 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001349 remotes = RunGit(['remote'], error_ok=True).split()
1350 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001352 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001353 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001354 logging.warning('Could not determine which remote this change is '
1355 'associated with, so defaulting to "%s". This may '
1356 'not be what you want. You may prevent this message '
1357 'by running "git svn info" as documented here: %s',
1358 self._remote,
1359 GIT_INSTRUCTIONS_URL)
1360 else:
1361 logging.warn('Could not determine which remote this change is '
1362 'associated with. You may prevent this message by '
1363 'running "git svn info" as documented here: %s',
1364 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001365 branch = 'HEAD'
1366 if branch.startswith('refs/remotes'):
1367 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001368 elif branch.startswith('refs/branch-heads/'):
1369 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001370 else:
1371 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001372 return self._remote
1373
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001374 def GitSanityChecks(self, upstream_git_obj):
1375 """Checks git repo status and ensures diff is from local commits."""
1376
sbc@chromium.org79706062015-01-14 21:18:12 +00001377 if upstream_git_obj is None:
1378 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001379 print('ERROR: unable to determine current branch (detached HEAD?)',
1380 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001381 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001382 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001383 return False
1384
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001385 # Verify the commit we're diffing against is in our current branch.
1386 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1387 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1388 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001389 print('ERROR: %s is not in the current branch. You may need to rebase '
1390 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001391 return False
1392
1393 # List the commits inside the diff, and verify they are all local.
1394 commits_in_diff = RunGit(
1395 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1396 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1397 remote_branch = remote_branch.strip()
1398 if code != 0:
1399 _, remote_branch = self.GetRemoteBranch()
1400
1401 commits_in_remote = RunGit(
1402 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1403
1404 common_commits = set(commits_in_diff) & set(commits_in_remote)
1405 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001406 print('ERROR: Your diff contains %d commits already in %s.\n'
1407 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1408 'the diff. If you are using a custom git flow, you can override'
1409 ' the reference used for this check with "git config '
1410 'gitcl.remotebranch <git-ref>".' % (
1411 len(common_commits), remote_branch, upstream_git_obj),
1412 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001413 return False
1414 return True
1415
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001416 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001417 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001418
1419 Returns None if it is not set.
1420 """
tandrii5d48c322016-08-18 16:19:37 -07001421 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001422
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001423 def GetGitSvnRemoteUrl(self):
1424 """Return the configured git-svn remote URL parsed from git svn info.
1425
1426 Returns None if it is not set.
1427 """
1428 # URL is dependent on the current directory.
1429 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1430 if data:
1431 keys = dict(line.split(': ', 1) for line in data.splitlines()
1432 if ': ' in line)
1433 return keys.get('URL', None)
1434 return None
1435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 def GetRemoteUrl(self):
1437 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1438
1439 Returns None if there is no remote.
1440 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001441 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001442 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1443
1444 # If URL is pointing to a local directory, it is probably a git cache.
1445 if os.path.isdir(url):
1446 url = RunGit(['config', 'remote.%s.url' % remote],
1447 error_ok=True,
1448 cwd=url).strip()
1449 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001451 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001452 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001453 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001454 self.issue = self._GitGetBranchConfigValue(
1455 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001456 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 return self.issue
1458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 def GetIssueURL(self):
1460 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001461 issue = self.GetIssue()
1462 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001463 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465
1466 def GetDescription(self, pretty=False):
1467 if not self.has_description:
1468 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001469 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 self.has_description = True
1471 if pretty:
1472 wrapper = textwrap.TextWrapper()
1473 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1474 return wrapper.fill(self.description)
1475 return self.description
1476
1477 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001478 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001480 self.patchset = self._GitGetBranchConfigValue(
1481 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001482 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 return self.patchset
1484
1485 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001486 """Set this branch's patchset. If patchset=0, clears the patchset."""
1487 assert self.GetBranch()
1488 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001489 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001490 else:
1491 self.patchset = int(patchset)
1492 self._GitSetBranchConfigValue(
1493 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001495 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001496 """Set this branch's issue. If issue isn't given, clears the issue."""
1497 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001499 issue = int(issue)
1500 self._GitSetBranchConfigValue(
1501 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001502 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001503 codereview_server = self._codereview_impl.GetCodereviewServer()
1504 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001505 self._GitSetBranchConfigValue(
1506 self._codereview_impl.CodereviewServerConfigKey(),
1507 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508 else:
tandrii5d48c322016-08-18 16:19:37 -07001509 # Reset all of these just to be clean.
1510 reset_suffixes = [
1511 'last-upload-hash',
1512 self._codereview_impl.IssueConfigKey(),
1513 self._codereview_impl.PatchsetConfigKey(),
1514 self._codereview_impl.CodereviewServerConfigKey(),
1515 ] + self._PostUnsetIssueProperties()
1516 for prop in reset_suffixes:
1517 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001518 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001519 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
dnjba1b0f32016-09-02 12:37:42 -07001521 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001522 if not self.GitSanityChecks(upstream_branch):
1523 DieWithError('\nGit sanity check failure')
1524
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001525 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001526 if not root:
1527 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001528 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001529
1530 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001531 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001532 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001533 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001534 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001535 except subprocess2.CalledProcessError:
1536 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001537 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001538 'This branch probably doesn\'t exist anymore. To reset the\n'
1539 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001540 ' git branch --set-upstream-to origin/master %s\n'
1541 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001542 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001543
maruel@chromium.org52424302012-08-29 15:14:30 +00001544 issue = self.GetIssue()
1545 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001546 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001547 description = self.GetDescription()
1548 else:
1549 # If the change was never uploaded, use the log messages of all commits
1550 # up to the branch point, as git cl upload will prefill the description
1551 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001552 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1553 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001554
1555 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001556 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001557 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001558 name,
1559 description,
1560 absroot,
1561 files,
1562 issue,
1563 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001564 author,
1565 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001566
dsansomee2d6fd92016-09-08 00:10:47 -07001567 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001568 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001569 return self._codereview_impl.UpdateDescriptionRemote(
1570 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001571
1572 def RunHook(self, committing, may_prompt, verbose, change):
1573 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1574 try:
1575 return presubmit_support.DoPresubmitChecks(change, committing,
1576 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1577 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001578 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1579 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001580 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001581 DieWithError(
1582 ('%s\nMaybe your depot_tools is out of date?\n'
1583 'If all fails, contact maruel@') % e)
1584
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001585 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1586 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001587 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1588 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001589 else:
1590 # Assume url.
1591 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1592 urlparse.urlparse(issue_arg))
1593 if not parsed_issue_arg or not parsed_issue_arg.valid:
1594 DieWithError('Failed to parse issue argument "%s". '
1595 'Must be an issue number or a valid URL.' % issue_arg)
1596 return self._codereview_impl.CMDPatchWithParsedIssue(
1597 parsed_issue_arg, reject, nocommit, directory)
1598
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599 def CMDUpload(self, options, git_diff_args, orig_args):
1600 """Uploads a change to codereview."""
1601 if git_diff_args:
1602 # TODO(ukai): is it ok for gerrit case?
1603 base_branch = git_diff_args[0]
1604 else:
1605 if self.GetBranch() is None:
1606 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1607
1608 # Default to diffing against common ancestor of upstream branch
1609 base_branch = self.GetCommonAncestorWithUpstream()
1610 git_diff_args = [base_branch, 'HEAD']
1611
1612 # Make sure authenticated to codereview before running potentially expensive
1613 # hooks. It is a fast, best efforts check. Codereview still can reject the
1614 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001615 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616
1617 # Apply watchlists on upload.
1618 change = self.GetChange(base_branch, None)
1619 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1620 files = [f.LocalPath() for f in change.AffectedFiles()]
1621 if not options.bypass_watchlists:
1622 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1623
1624 if not options.bypass_hooks:
1625 if options.reviewers or options.tbr_owners:
1626 # Set the reviewer list now so that presubmit checks can access it.
1627 change_description = ChangeDescription(change.FullDescriptionText())
1628 change_description.update_reviewers(options.reviewers,
1629 options.tbr_owners,
1630 change)
1631 change.SetDescriptionText(change_description.description)
1632 hook_results = self.RunHook(committing=False,
1633 may_prompt=not options.force,
1634 verbose=options.verbose,
1635 change=change)
1636 if not hook_results.should_continue():
1637 return 1
1638 if not options.reviewers and hook_results.reviewers:
1639 options.reviewers = hook_results.reviewers.split(',')
1640
1641 if self.GetIssue():
1642 latest_patchset = self.GetMostRecentPatchset()
1643 local_patchset = self.GetPatchset()
1644 if (latest_patchset and local_patchset and
1645 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001646 print('The last upload made from this repository was patchset #%d but '
1647 'the most recent patchset on the server is #%d.'
1648 % (local_patchset, latest_patchset))
1649 print('Uploading will still work, but if you\'ve uploaded to this '
1650 'issue from another machine or branch the patch you\'re '
1651 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 ask_for_data('About to upload; enter to confirm.')
1653
1654 print_stats(options.similarity, options.find_copies, git_diff_args)
1655 ret = self.CMDUploadChange(options, git_diff_args, change)
1656 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001657 if options.use_commit_queue:
1658 self.SetCQState(_CQState.COMMIT)
1659 elif options.cq_dry_run:
1660 self.SetCQState(_CQState.DRY_RUN)
1661
tandrii5d48c322016-08-18 16:19:37 -07001662 _git_set_branch_config_value('last-upload-hash',
1663 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001664 # Run post upload hooks, if specified.
1665 if settings.GetRunPostUploadHook():
1666 presubmit_support.DoPostUploadExecuter(
1667 change,
1668 self,
1669 settings.GetRoot(),
1670 options.verbose,
1671 sys.stdout)
1672
1673 # Upload all dependencies if specified.
1674 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001675 print()
1676 print('--dependencies has been specified.')
1677 print('All dependent local branches will be re-uploaded.')
1678 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001679 # Remove the dependencies flag from args so that we do not end up in a
1680 # loop.
1681 orig_args.remove('--dependencies')
1682 ret = upload_branch_deps(self, orig_args)
1683 return ret
1684
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001685 def SetCQState(self, new_state):
1686 """Update the CQ state for latest patchset.
1687
1688 Issue must have been already uploaded and known.
1689 """
1690 assert new_state in _CQState.ALL_STATES
1691 assert self.GetIssue()
1692 return self._codereview_impl.SetCQState(new_state)
1693
qyearsley1fdfcb62016-10-24 13:22:03 -07001694 def TriggerDryRun(self):
1695 """Triggers a dry run and prints a warning on failure."""
1696 # TODO(qyearsley): Either re-use this method in CMDset_commit
1697 # and CMDupload, or change CMDtry to trigger dry runs with
1698 # just SetCQState, and catch keyboard interrupt and other
1699 # errors in that method.
1700 try:
1701 self.SetCQState(_CQState.DRY_RUN)
1702 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1703 return 0
1704 except KeyboardInterrupt:
1705 raise
1706 except:
1707 print('WARNING: failed to trigger CQ Dry Run.\n'
1708 'Either:\n'
1709 ' * your project has no CQ\n'
1710 ' * you don\'t have permission to trigger Dry Run\n'
1711 ' * bug in this code (see stack trace below).\n'
1712 'Consider specifying which bots to trigger manually '
1713 'or asking your project owners for permissions '
1714 'or contacting Chrome Infrastructure team at '
1715 'https://www.chromium.org/infra\n\n')
1716 # Still raise exception so that stack trace is printed.
1717 raise
1718
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 # Forward methods to codereview specific implementation.
1720
1721 def CloseIssue(self):
1722 return self._codereview_impl.CloseIssue()
1723
1724 def GetStatus(self):
1725 return self._codereview_impl.GetStatus()
1726
1727 def GetCodereviewServer(self):
1728 return self._codereview_impl.GetCodereviewServer()
1729
tandriide281ae2016-10-12 06:02:30 -07001730 def GetIssueOwner(self):
1731 """Get owner from codereview, which may differ from this checkout."""
1732 return self._codereview_impl.GetIssueOwner()
1733
1734 def GetIssueProject(self):
1735 """Get project from codereview, which may differ from what this
1736 checkout's codereview.settings or gerrit project URL say.
1737 """
1738 return self._codereview_impl.GetIssueProject()
1739
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 def GetApprovingReviewers(self):
1741 return self._codereview_impl.GetApprovingReviewers()
1742
1743 def GetMostRecentPatchset(self):
1744 return self._codereview_impl.GetMostRecentPatchset()
1745
tandriide281ae2016-10-12 06:02:30 -07001746 def CannotTriggerTryJobReason(self):
1747 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1748 return self._codereview_impl.CannotTriggerTryJobReason()
1749
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 def __getattr__(self, attr):
1751 # This is because lots of untested code accesses Rietveld-specific stuff
1752 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001753 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001754 # Note that child method defines __getattr__ as well, and forwards it here,
1755 # because _RietveldChangelistImpl is not cleaned up yet, and given
1756 # deprecation of Rietveld, it should probably be just removed.
1757 # Until that time, avoid infinite recursion by bypassing __getattr__
1758 # of implementation class.
1759 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760
1761
1762class _ChangelistCodereviewBase(object):
1763 """Abstract base class encapsulating codereview specifics of a changelist."""
1764 def __init__(self, changelist):
1765 self._changelist = changelist # instance of Changelist
1766
1767 def __getattr__(self, attr):
1768 # Forward methods to changelist.
1769 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1770 # _RietveldChangelistImpl to avoid this hack?
1771 return getattr(self._changelist, attr)
1772
1773 def GetStatus(self):
1774 """Apply a rough heuristic to give a simple summary of an issue's review
1775 or CQ status, assuming adherence to a common workflow.
1776
1777 Returns None if no issue for this branch, or specific string keywords.
1778 """
1779 raise NotImplementedError()
1780
1781 def GetCodereviewServer(self):
1782 """Returns server URL without end slash, like "https://codereview.com"."""
1783 raise NotImplementedError()
1784
1785 def FetchDescription(self):
1786 """Fetches and returns description from the codereview server."""
1787 raise NotImplementedError()
1788
tandrii5d48c322016-08-18 16:19:37 -07001789 @classmethod
1790 def IssueConfigKey(cls):
1791 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001792 raise NotImplementedError()
1793
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001794 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001795 def PatchsetConfigKey(cls):
1796 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 raise NotImplementedError()
1798
tandrii5d48c322016-08-18 16:19:37 -07001799 @classmethod
1800 def CodereviewServerConfigKey(cls):
1801 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 raise NotImplementedError()
1803
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001804 def _PostUnsetIssueProperties(self):
1805 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001806 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001807
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 def GetRieveldObjForPresubmit(self):
1809 # This is an unfortunate Rietveld-embeddedness in presubmit.
1810 # For non-Rietveld codereviews, this probably should return a dummy object.
1811 raise NotImplementedError()
1812
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001813 def GetGerritObjForPresubmit(self):
1814 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1815 return None
1816
dsansomee2d6fd92016-09-08 00:10:47 -07001817 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 """Update the description on codereview site."""
1819 raise NotImplementedError()
1820
1821 def CloseIssue(self):
1822 """Closes the issue."""
1823 raise NotImplementedError()
1824
1825 def GetApprovingReviewers(self):
1826 """Returns a list of reviewers approving the change.
1827
1828 Note: not necessarily committers.
1829 """
1830 raise NotImplementedError()
1831
1832 def GetMostRecentPatchset(self):
1833 """Returns the most recent patchset number from the codereview site."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001836 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1837 directory):
1838 """Fetches and applies the issue.
1839
1840 Arguments:
1841 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1842 reject: if True, reject the failed patch instead of switching to 3-way
1843 merge. Rietveld only.
1844 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1845 only.
1846 directory: switch to directory before applying the patch. Rietveld only.
1847 """
1848 raise NotImplementedError()
1849
1850 @staticmethod
1851 def ParseIssueURL(parsed_url):
1852 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1853 failed."""
1854 raise NotImplementedError()
1855
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001856 def EnsureAuthenticated(self, force):
1857 """Best effort check that user is authenticated with codereview server.
1858
1859 Arguments:
1860 force: whether to skip confirmation questions.
1861 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862 raise NotImplementedError()
1863
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001864 def CMDUploadChange(self, options, args, change):
1865 """Uploads a change to codereview."""
1866 raise NotImplementedError()
1867
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001868 def SetCQState(self, new_state):
1869 """Update the CQ state for latest patchset.
1870
1871 Issue must have been already uploaded and known.
1872 """
1873 raise NotImplementedError()
1874
tandriie113dfd2016-10-11 10:20:12 -07001875 def CannotTriggerTryJobReason(self):
1876 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1877 raise NotImplementedError()
1878
tandriide281ae2016-10-12 06:02:30 -07001879 def GetIssueOwner(self):
1880 raise NotImplementedError()
1881
1882 def GetIssueProject(self):
1883 raise NotImplementedError()
1884
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885
1886class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1887 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1888 super(_RietveldChangelistImpl, self).__init__(changelist)
1889 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001890 if not rietveld_server:
1891 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001892
1893 self._rietveld_server = rietveld_server
1894 self._auth_config = auth_config
1895 self._props = None
1896 self._rpc_server = None
1897
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 def GetCodereviewServer(self):
1899 if not self._rietveld_server:
1900 # If we're on a branch then get the server potentially associated
1901 # with that branch.
1902 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001903 self._rietveld_server = gclient_utils.UpgradeToHttps(
1904 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 if not self._rietveld_server:
1906 self._rietveld_server = settings.GetDefaultServerUrl()
1907 return self._rietveld_server
1908
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001909 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001910 """Best effort check that user is authenticated with Rietveld server."""
1911 if self._auth_config.use_oauth2:
1912 authenticator = auth.get_authenticator_for_host(
1913 self.GetCodereviewServer(), self._auth_config)
1914 if not authenticator.has_cached_credentials():
1915 raise auth.LoginRequiredError(self.GetCodereviewServer())
1916
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001917 def FetchDescription(self):
1918 issue = self.GetIssue()
1919 assert issue
1920 try:
1921 return self.RpcServer().get_description(issue).strip()
1922 except urllib2.HTTPError as e:
1923 if e.code == 404:
1924 DieWithError(
1925 ('\nWhile fetching the description for issue %d, received a '
1926 '404 (not found)\n'
1927 'error. It is likely that you deleted this '
1928 'issue on the server. If this is the\n'
1929 'case, please run\n\n'
1930 ' git cl issue 0\n\n'
1931 'to clear the association with the deleted issue. Then run '
1932 'this command again.') % issue)
1933 else:
1934 DieWithError(
1935 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1936 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001937 print('Warning: Failed to retrieve CL description due to network '
1938 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939 return ''
1940
1941 def GetMostRecentPatchset(self):
1942 return self.GetIssueProperties()['patchsets'][-1]
1943
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001944 def GetIssueProperties(self):
1945 if self._props is None:
1946 issue = self.GetIssue()
1947 if not issue:
1948 self._props = {}
1949 else:
1950 self._props = self.RpcServer().get_issue_properties(issue, True)
1951 return self._props
1952
tandriie113dfd2016-10-11 10:20:12 -07001953 def CannotTriggerTryJobReason(self):
1954 props = self.GetIssueProperties()
1955 if not props:
1956 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1957 if props.get('closed'):
1958 return 'CL %s is closed' % self.GetIssue()
1959 if props.get('private'):
1960 return 'CL %s is private' % self.GetIssue()
1961 return None
1962
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001963 def GetApprovingReviewers(self):
1964 return get_approving_reviewers(self.GetIssueProperties())
1965
tandriide281ae2016-10-12 06:02:30 -07001966 def GetIssueOwner(self):
1967 return (self.GetIssueProperties() or {}).get('owner_email')
1968
1969 def GetIssueProject(self):
1970 return (self.GetIssueProperties() or {}).get('project')
1971
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001972 def AddComment(self, message):
1973 return self.RpcServer().add_comment(self.GetIssue(), message)
1974
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001975 def GetStatus(self):
1976 """Apply a rough heuristic to give a simple summary of an issue's review
1977 or CQ status, assuming adherence to a common workflow.
1978
1979 Returns None if no issue for this branch, or one of the following keywords:
1980 * 'error' - error from review tool (including deleted issues)
1981 * 'unsent' - not sent for review
1982 * 'waiting' - waiting for review
1983 * 'reply' - waiting for owner to reply to review
1984 * 'lgtm' - LGTM from at least one approved reviewer
1985 * 'commit' - in the commit queue
1986 * 'closed' - closed
1987 """
1988 if not self.GetIssue():
1989 return None
1990
1991 try:
1992 props = self.GetIssueProperties()
1993 except urllib2.HTTPError:
1994 return 'error'
1995
1996 if props.get('closed'):
1997 # Issue is closed.
1998 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001999 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002000 # Issue is in the commit queue.
2001 return 'commit'
2002
2003 try:
2004 reviewers = self.GetApprovingReviewers()
2005 except urllib2.HTTPError:
2006 return 'error'
2007
2008 if reviewers:
2009 # Was LGTM'ed.
2010 return 'lgtm'
2011
2012 messages = props.get('messages') or []
2013
tandrii9d2c7a32016-06-22 03:42:45 -07002014 # Skip CQ messages that don't require owner's action.
2015 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2016 if 'Dry run:' in messages[-1]['text']:
2017 messages.pop()
2018 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2019 # This message always follows prior messages from CQ,
2020 # so skip this too.
2021 messages.pop()
2022 else:
2023 # This is probably a CQ messages warranting user attention.
2024 break
2025
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002026 if not messages:
2027 # No message was sent.
2028 return 'unsent'
2029 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002030 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002031 return 'reply'
2032 return 'waiting'
2033
dsansomee2d6fd92016-09-08 00:10:47 -07002034 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002035 return self.RpcServer().update_description(
2036 self.GetIssue(), self.description)
2037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002039 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002041 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002042 return self.SetFlags({flag: value})
2043
2044 def SetFlags(self, flags):
2045 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002046 """
phajdan.jr68598232016-08-10 03:28:28 -07002047 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002048 try:
tandrii4b233bd2016-07-06 03:50:29 -07002049 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002050 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002051 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002052 if e.code == 404:
2053 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2054 if e.code == 403:
2055 DieWithError(
2056 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002057 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002058 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002060 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061 """Returns an upload.RpcServer() to access this review's rietveld instance.
2062 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002063 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002064 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002065 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002066 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002067 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002069 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002070 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002071 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072
tandrii5d48c322016-08-18 16:19:37 -07002073 @classmethod
2074 def PatchsetConfigKey(cls):
2075 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076
tandrii5d48c322016-08-18 16:19:37 -07002077 @classmethod
2078 def CodereviewServerConfigKey(cls):
2079 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002080
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081 def GetRieveldObjForPresubmit(self):
2082 return self.RpcServer()
2083
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002084 def SetCQState(self, new_state):
2085 props = self.GetIssueProperties()
2086 if props.get('private'):
2087 DieWithError('Cannot set-commit on private issue')
2088
2089 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002090 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002091 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002092 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002093 else:
tandrii4b233bd2016-07-06 03:50:29 -07002094 assert new_state == _CQState.DRY_RUN
2095 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002096
2097
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002098 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2099 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002100 # PatchIssue should never be called with a dirty tree. It is up to the
2101 # caller to check this, but just in case we assert here since the
2102 # consequences of the caller not checking this could be dire.
2103 assert(not git_common.is_dirty_git_tree('apply'))
2104 assert(parsed_issue_arg.valid)
2105 self._changelist.issue = parsed_issue_arg.issue
2106 if parsed_issue_arg.hostname:
2107 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2108
skobes6468b902016-10-24 08:45:10 -07002109 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2110 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2111 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 try:
skobes6468b902016-10-24 08:45:10 -07002113 scm_obj.apply_patch(patchset_object)
2114 except Exception as e:
2115 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 return 1
2117
2118 # If we had an issue, commit the current state and register the issue.
2119 if not nocommit:
2120 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2121 'patch from issue %(i)s at patchset '
2122 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2123 % {'i': self.GetIssue(), 'p': patchset})])
2124 self.SetIssue(self.GetIssue())
2125 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002126 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002127 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002128 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 return 0
2130
2131 @staticmethod
2132 def ParseIssueURL(parsed_url):
2133 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2134 return None
wychen3c1c1722016-08-04 11:46:36 -07002135 # Rietveld patch: https://domain/<number>/#ps<patchset>
2136 match = re.match(r'/(\d+)/$', parsed_url.path)
2137 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2138 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002139 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002140 issue=int(match.group(1)),
2141 patchset=int(match2.group(1)),
2142 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002143 # Typical url: https://domain/<issue_number>[/[other]]
2144 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2145 if match:
skobes6468b902016-10-24 08:45:10 -07002146 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002147 issue=int(match.group(1)),
2148 hostname=parsed_url.netloc)
2149 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2150 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2151 if match:
skobes6468b902016-10-24 08:45:10 -07002152 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002153 issue=int(match.group(1)),
2154 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002155 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 return None
2157
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 def CMDUploadChange(self, options, args, change):
2159 """Upload the patch to Rietveld."""
2160 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2161 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002162 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2163 if options.emulate_svn_auto_props:
2164 upload_args.append('--emulate_svn_auto_props')
2165
2166 change_desc = None
2167
2168 if options.email is not None:
2169 upload_args.extend(['--email', options.email])
2170
2171 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002172 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002173 upload_args.extend(['--title', options.title])
2174 if options.message:
2175 upload_args.extend(['--message', options.message])
2176 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002177 print('This branch is associated with issue %s. '
2178 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002179 else:
nodirca166002016-06-27 10:59:51 -07002180 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002181 upload_args.extend(['--title', options.title])
2182 message = (options.title or options.message or
2183 CreateDescriptionFromLog(args))
2184 change_desc = ChangeDescription(message)
2185 if options.reviewers or options.tbr_owners:
2186 change_desc.update_reviewers(options.reviewers,
2187 options.tbr_owners,
2188 change)
2189 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002190 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191
2192 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002193 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 return 1
2195
2196 upload_args.extend(['--message', change_desc.description])
2197 if change_desc.get_reviewers():
2198 upload_args.append('--reviewers=%s' % ','.join(
2199 change_desc.get_reviewers()))
2200 if options.send_mail:
2201 if not change_desc.get_reviewers():
2202 DieWithError("Must specify reviewers to send email.")
2203 upload_args.append('--send_mail')
2204
2205 # We check this before applying rietveld.private assuming that in
2206 # rietveld.cc only addresses which we can send private CLs to are listed
2207 # if rietveld.private is set, and so we should ignore rietveld.cc only
2208 # when --private is specified explicitly on the command line.
2209 if options.private:
2210 logging.warn('rietveld.cc is ignored since private flag is specified. '
2211 'You need to review and add them manually if necessary.')
2212 cc = self.GetCCListWithoutDefault()
2213 else:
2214 cc = self.GetCCList()
2215 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002216 if change_desc.get_cced():
2217 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002218 if cc:
2219 upload_args.extend(['--cc', cc])
2220
2221 if options.private or settings.GetDefaultPrivateFlag() == "True":
2222 upload_args.append('--private')
2223
2224 upload_args.extend(['--git_similarity', str(options.similarity)])
2225 if not options.find_copies:
2226 upload_args.extend(['--git_no_find_copies'])
2227
2228 # Include the upstream repo's URL in the change -- this is useful for
2229 # projects that have their source spread across multiple repos.
2230 remote_url = self.GetGitBaseUrlFromConfig()
2231 if not remote_url:
2232 if settings.GetIsGitSvn():
2233 remote_url = self.GetGitSvnRemoteUrl()
2234 else:
2235 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2236 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2237 self.GetUpstreamBranch().split('/')[-1])
2238 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 remote, remote_branch = self.GetRemoteBranch()
2240 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2241 settings.GetPendingRefPrefix())
2242 if target_ref:
2243 upload_args.extend(['--target_ref', target_ref])
2244
2245 # Look for dependent patchsets. See crbug.com/480453 for more details.
2246 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2247 upstream_branch = ShortBranchName(upstream_branch)
2248 if remote is '.':
2249 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002250 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002252 print()
2253 print('Skipping dependency patchset upload because git config '
2254 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2255 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002256 else:
2257 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002258 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002259 auth_config=auth_config)
2260 branch_cl_issue_url = branch_cl.GetIssueURL()
2261 branch_cl_issue = branch_cl.GetIssue()
2262 branch_cl_patchset = branch_cl.GetPatchset()
2263 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2264 upload_args.extend(
2265 ['--depends_on_patchset', '%s:%s' % (
2266 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002267 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 '\n'
2269 'The current branch (%s) is tracking a local branch (%s) with '
2270 'an associated CL.\n'
2271 'Adding %s/#ps%s as a dependency patchset.\n'
2272 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2273 branch_cl_patchset))
2274
2275 project = settings.GetProject()
2276 if project:
2277 upload_args.extend(['--project', project])
2278
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002279 try:
2280 upload_args = ['upload'] + upload_args + args
2281 logging.info('upload.RealMain(%s)', upload_args)
2282 issue, patchset = upload.RealMain(upload_args)
2283 issue = int(issue)
2284 patchset = int(patchset)
2285 except KeyboardInterrupt:
2286 sys.exit(1)
2287 except:
2288 # If we got an exception after the user typed a description for their
2289 # change, back up the description before re-raising.
2290 if change_desc:
2291 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2292 print('\nGot exception while uploading -- saving description to %s\n' %
2293 backup_path)
2294 backup_file = open(backup_path, 'w')
2295 backup_file.write(change_desc.description)
2296 backup_file.close()
2297 raise
2298
2299 if not self.GetIssue():
2300 self.SetIssue(issue)
2301 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 return 0
2303
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002304
2305class _GerritChangelistImpl(_ChangelistCodereviewBase):
2306 def __init__(self, changelist, auth_config=None):
2307 # auth_config is Rietveld thing, kept here to preserve interface only.
2308 super(_GerritChangelistImpl, self).__init__(changelist)
2309 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002310 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002311 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002312 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002313
2314 def _GetGerritHost(self):
2315 # Lazy load of configs.
2316 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002317 if self._gerrit_host and '.' not in self._gerrit_host:
2318 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2319 # This happens for internal stuff http://crbug.com/614312.
2320 parsed = urlparse.urlparse(self.GetRemoteUrl())
2321 if parsed.scheme == 'sso':
2322 print('WARNING: using non https URLs for remote is likely broken\n'
2323 ' Your current remote is: %s' % self.GetRemoteUrl())
2324 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2325 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002326 return self._gerrit_host
2327
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002328 def _GetGitHost(self):
2329 """Returns git host to be used when uploading change to Gerrit."""
2330 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2331
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002332 def GetCodereviewServer(self):
2333 if not self._gerrit_server:
2334 # If we're on a branch then get the server potentially associated
2335 # with that branch.
2336 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002337 self._gerrit_server = self._GitGetBranchConfigValue(
2338 self.CodereviewServerConfigKey())
2339 if self._gerrit_server:
2340 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002341 if not self._gerrit_server:
2342 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2343 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002344 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002345 parts[0] = parts[0] + '-review'
2346 self._gerrit_host = '.'.join(parts)
2347 self._gerrit_server = 'https://%s' % self._gerrit_host
2348 return self._gerrit_server
2349
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002350 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002351 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002352 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002353
tandrii5d48c322016-08-18 16:19:37 -07002354 @classmethod
2355 def PatchsetConfigKey(cls):
2356 return 'gerritpatchset'
2357
2358 @classmethod
2359 def CodereviewServerConfigKey(cls):
2360 return 'gerritserver'
2361
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002362 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002363 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002364 if settings.GetGerritSkipEnsureAuthenticated():
2365 # For projects with unusual authentication schemes.
2366 # See http://crbug.com/603378.
2367 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002368 # Lazy-loader to identify Gerrit and Git hosts.
2369 if gerrit_util.GceAuthenticator.is_gce():
2370 return
2371 self.GetCodereviewServer()
2372 git_host = self._GetGitHost()
2373 assert self._gerrit_server and self._gerrit_host
2374 cookie_auth = gerrit_util.CookiesAuthenticator()
2375
2376 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2377 git_auth = cookie_auth.get_auth_header(git_host)
2378 if gerrit_auth and git_auth:
2379 if gerrit_auth == git_auth:
2380 return
2381 print((
2382 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2383 ' Check your %s or %s file for credentials of hosts:\n'
2384 ' %s\n'
2385 ' %s\n'
2386 ' %s') %
2387 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2388 git_host, self._gerrit_host,
2389 cookie_auth.get_new_password_message(git_host)))
2390 if not force:
2391 ask_for_data('If you know what you are doing, press Enter to continue, '
2392 'Ctrl+C to abort.')
2393 return
2394 else:
2395 missing = (
2396 [] if gerrit_auth else [self._gerrit_host] +
2397 [] if git_auth else [git_host])
2398 DieWithError('Credentials for the following hosts are required:\n'
2399 ' %s\n'
2400 'These are read from %s (or legacy %s)\n'
2401 '%s' % (
2402 '\n '.join(missing),
2403 cookie_auth.get_gitcookies_path(),
2404 cookie_auth.get_netrc_path(),
2405 cookie_auth.get_new_password_message(git_host)))
2406
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002407 def _PostUnsetIssueProperties(self):
2408 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002409 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002410
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002411 def GetRieveldObjForPresubmit(self):
2412 class ThisIsNotRietveldIssue(object):
2413 def __nonzero__(self):
2414 # This is a hack to make presubmit_support think that rietveld is not
2415 # defined, yet still ensure that calls directly result in a decent
2416 # exception message below.
2417 return False
2418
2419 def __getattr__(self, attr):
2420 print(
2421 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2422 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2423 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2424 'or use Rietveld for codereview.\n'
2425 'See also http://crbug.com/579160.' % attr)
2426 raise NotImplementedError()
2427 return ThisIsNotRietveldIssue()
2428
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002429 def GetGerritObjForPresubmit(self):
2430 return presubmit_support.GerritAccessor(self._GetGerritHost())
2431
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002432 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433 """Apply a rough heuristic to give a simple summary of an issue's review
2434 or CQ status, assuming adherence to a common workflow.
2435
2436 Returns None if no issue for this branch, or one of the following keywords:
2437 * 'error' - error from review tool (including deleted issues)
2438 * 'unsent' - no reviewers added
2439 * 'waiting' - waiting for review
2440 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002441 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2442 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002443 * 'commit' - in the commit queue
2444 * 'closed' - abandoned
2445 """
2446 if not self.GetIssue():
2447 return None
2448
2449 try:
2450 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002451 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002452 return 'error'
2453
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002454 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002455 return 'closed'
2456
2457 cq_label = data['labels'].get('Commit-Queue', {})
2458 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002459 votes = cq_label.get('all', [])
2460 highest_vote = 0
2461 for v in votes:
2462 highest_vote = max(highest_vote, v.get('value', 0))
2463 vote_value = str(highest_vote)
2464 if vote_value != '0':
2465 # Add a '+' if the value is not 0 to match the values in the label.
2466 # The cq_label does not have negatives.
2467 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002468 vote_text = cq_label.get('values', {}).get(vote_value, '')
2469 if vote_text.lower() == 'commit':
2470 return 'commit'
2471
2472 lgtm_label = data['labels'].get('Code-Review', {})
2473 if lgtm_label:
2474 if 'rejected' in lgtm_label:
2475 return 'not lgtm'
2476 if 'approved' in lgtm_label:
2477 return 'lgtm'
2478
2479 if not data.get('reviewers', {}).get('REVIEWER', []):
2480 return 'unsent'
2481
2482 messages = data.get('messages', [])
2483 if messages:
2484 owner = data['owner'].get('_account_id')
2485 last_message_author = messages[-1].get('author', {}).get('_account_id')
2486 if owner != last_message_author:
2487 # Some reply from non-owner.
2488 return 'reply'
2489
2490 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002491
2492 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002493 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002494 return data['revisions'][data['current_revision']]['_number']
2495
2496 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002497 data = self._GetChangeDetail(['CURRENT_REVISION'])
2498 current_rev = data['current_revision']
2499 url = data['revisions'][current_rev]['fetch']['http']['url']
2500 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002501
dsansomee2d6fd92016-09-08 00:10:47 -07002502 def UpdateDescriptionRemote(self, description, force=False):
2503 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2504 if not force:
2505 ask_for_data(
2506 'The description cannot be modified while the issue has a pending '
2507 'unpublished edit. Either publish the edit in the Gerrit web UI '
2508 'or delete it.\n\n'
2509 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2510
2511 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2512 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002513 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2514 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002515
2516 def CloseIssue(self):
2517 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2518
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002519 def GetApprovingReviewers(self):
2520 """Returns a list of reviewers approving the change.
2521
2522 Note: not necessarily committers.
2523 """
2524 raise NotImplementedError()
2525
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002526 def SubmitIssue(self, wait_for_merge=True):
2527 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2528 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002529
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002530 def _GetChangeDetail(self, options=None, issue=None):
2531 options = options or []
2532 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002533 assert issue, 'issue is required to query Gerrit'
2534 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002535 options)
tandriic2405f52016-10-10 08:13:15 -07002536 if not data:
2537 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2538 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002539
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002540 def CMDLand(self, force, bypass_hooks, verbose):
2541 if git_common.is_dirty_git_tree('land'):
2542 return 1
tandriid60367b2016-06-22 05:25:12 -07002543 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2544 if u'Commit-Queue' in detail.get('labels', {}):
2545 if not force:
2546 ask_for_data('\nIt seems this repository has a Commit Queue, '
2547 'which can test and land changes for you. '
2548 'Are you sure you wish to bypass it?\n'
2549 'Press Enter to continue, Ctrl+C to abort.')
2550
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002551 differs = True
tandriic4344b52016-08-29 06:04:54 -07002552 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002553 # Note: git diff outputs nothing if there is no diff.
2554 if not last_upload or RunGit(['diff', last_upload]).strip():
2555 print('WARNING: some changes from local branch haven\'t been uploaded')
2556 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002557 if detail['current_revision'] == last_upload:
2558 differs = False
2559 else:
2560 print('WARNING: local branch contents differ from latest uploaded '
2561 'patchset')
2562 if differs:
2563 if not force:
2564 ask_for_data(
2565 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2566 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2567 elif not bypass_hooks:
2568 hook_results = self.RunHook(
2569 committing=True,
2570 may_prompt=not force,
2571 verbose=verbose,
2572 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2573 if not hook_results.should_continue():
2574 return 1
2575
2576 self.SubmitIssue(wait_for_merge=True)
2577 print('Issue %s has been submitted.' % self.GetIssueURL())
2578 return 0
2579
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002580 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2581 directory):
2582 assert not reject
2583 assert not nocommit
2584 assert not directory
2585 assert parsed_issue_arg.valid
2586
2587 self._changelist.issue = parsed_issue_arg.issue
2588
2589 if parsed_issue_arg.hostname:
2590 self._gerrit_host = parsed_issue_arg.hostname
2591 self._gerrit_server = 'https://%s' % self._gerrit_host
2592
tandriic2405f52016-10-10 08:13:15 -07002593 try:
2594 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2595 except GerritIssueNotExists as e:
2596 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002597
2598 if not parsed_issue_arg.patchset:
2599 # Use current revision by default.
2600 revision_info = detail['revisions'][detail['current_revision']]
2601 patchset = int(revision_info['_number'])
2602 else:
2603 patchset = parsed_issue_arg.patchset
2604 for revision_info in detail['revisions'].itervalues():
2605 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2606 break
2607 else:
2608 DieWithError('Couldn\'t find patchset %i in issue %i' %
2609 (parsed_issue_arg.patchset, self.GetIssue()))
2610
2611 fetch_info = revision_info['fetch']['http']
2612 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2613 RunGit(['cherry-pick', 'FETCH_HEAD'])
2614 self.SetIssue(self.GetIssue())
2615 self.SetPatchset(patchset)
2616 print('Committed patch for issue %i pathset %i locally' %
2617 (self.GetIssue(), self.GetPatchset()))
2618 return 0
2619
2620 @staticmethod
2621 def ParseIssueURL(parsed_url):
2622 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2623 return None
2624 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2625 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2626 # Short urls like https://domain/<issue_number> can be used, but don't allow
2627 # specifying the patchset (you'd 404), but we allow that here.
2628 if parsed_url.path == '/':
2629 part = parsed_url.fragment
2630 else:
2631 part = parsed_url.path
2632 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2633 if match:
2634 return _ParsedIssueNumberArgument(
2635 issue=int(match.group(2)),
2636 patchset=int(match.group(4)) if match.group(4) else None,
2637 hostname=parsed_url.netloc)
2638 return None
2639
tandrii16e0b4e2016-06-07 10:34:28 -07002640 def _GerritCommitMsgHookCheck(self, offer_removal):
2641 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2642 if not os.path.exists(hook):
2643 return
2644 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2645 # custom developer made one.
2646 data = gclient_utils.FileRead(hook)
2647 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2648 return
2649 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002650 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002651 'and may interfere with it in subtle ways.\n'
2652 'We recommend you remove the commit-msg hook.')
2653 if offer_removal:
2654 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2655 if reply.lower().startswith('y'):
2656 gclient_utils.rm_file_or_tree(hook)
2657 print('Gerrit commit-msg hook removed.')
2658 else:
2659 print('OK, will keep Gerrit commit-msg hook in place.')
2660
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661 def CMDUploadChange(self, options, args, change):
2662 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002663 if options.squash and options.no_squash:
2664 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002665
2666 if not options.squash and not options.no_squash:
2667 # Load default for user, repo, squash=true, in this order.
2668 options.squash = settings.GetSquashGerritUploads()
2669 elif options.no_squash:
2670 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002671
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002672 # We assume the remote called "origin" is the one we want.
2673 # It is probably not worthwhile to support different workflows.
2674 gerrit_remote = 'origin'
2675
2676 remote, remote_branch = self.GetRemoteBranch()
2677 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2678 pending_prefix='')
2679
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002680 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002681 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002682 if self.GetIssue():
2683 # Try to get the message from a previous upload.
2684 message = self.GetDescription()
2685 if not message:
2686 DieWithError(
2687 'failed to fetch description from current Gerrit issue %d\n'
2688 '%s' % (self.GetIssue(), self.GetIssueURL()))
2689 change_id = self._GetChangeDetail()['change_id']
2690 while True:
2691 footer_change_ids = git_footers.get_footer_change_id(message)
2692 if footer_change_ids == [change_id]:
2693 break
2694 if not footer_change_ids:
2695 message = git_footers.add_footer_change_id(message, change_id)
2696 print('WARNING: appended missing Change-Id to issue description')
2697 continue
2698 # There is already a valid footer but with different or several ids.
2699 # Doing this automatically is non-trivial as we don't want to lose
2700 # existing other footers, yet we want to append just 1 desired
2701 # Change-Id. Thus, just create a new footer, but let user verify the
2702 # new description.
2703 message = '%s\n\nChange-Id: %s' % (message, change_id)
2704 print(
2705 'WARNING: issue %s has Change-Id footer(s):\n'
2706 ' %s\n'
2707 'but issue has Change-Id %s, according to Gerrit.\n'
2708 'Please, check the proposed correction to the description, '
2709 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2710 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2711 change_id))
2712 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2713 if not options.force:
2714 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002715 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002716 message = change_desc.description
2717 if not message:
2718 DieWithError("Description is empty. Aborting...")
2719 # Continue the while loop.
2720 # Sanity check of this code - we should end up with proper message
2721 # footer.
2722 assert [change_id] == git_footers.get_footer_change_id(message)
2723 change_desc = ChangeDescription(message)
2724 else:
2725 change_desc = ChangeDescription(
2726 options.message or CreateDescriptionFromLog(args))
2727 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002728 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729 if not change_desc.description:
2730 DieWithError("Description is empty. Aborting...")
2731 message = change_desc.description
2732 change_ids = git_footers.get_footer_change_id(message)
2733 if len(change_ids) > 1:
2734 DieWithError('too many Change-Id footers, at most 1 allowed.')
2735 if not change_ids:
2736 # Generate the Change-Id automatically.
2737 message = git_footers.add_footer_change_id(
2738 message, GenerateGerritChangeId(message))
2739 change_desc.set_description(message)
2740 change_ids = git_footers.get_footer_change_id(message)
2741 assert len(change_ids) == 1
2742 change_id = change_ids[0]
2743
2744 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2745 if remote is '.':
2746 # If our upstream branch is local, we base our squashed commit on its
2747 # squashed version.
2748 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2749 # Check the squashed hash of the parent.
2750 parent = RunGit(['config',
2751 'branch.%s.gerritsquashhash' % upstream_branch_name],
2752 error_ok=True).strip()
2753 # Verify that the upstream branch has been uploaded too, otherwise
2754 # Gerrit will create additional CLs when uploading.
2755 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2756 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 DieWithError(
2758 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002759 'Note: maybe you\'ve uploaded it with --no-squash. '
2760 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 ' git cl upload --squash\n' % upstream_branch_name)
2762 else:
2763 parent = self.GetCommonAncestorWithUpstream()
2764
2765 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2766 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2767 '-m', message]).strip()
2768 else:
2769 change_desc = ChangeDescription(
2770 options.message or CreateDescriptionFromLog(args))
2771 if not change_desc.description:
2772 DieWithError("Description is empty. Aborting...")
2773
2774 if not git_footers.get_footer_change_id(change_desc.description):
2775 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002776 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2777 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002778 ref_to_push = 'HEAD'
2779 parent = '%s/%s' % (gerrit_remote, branch)
2780 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2781
2782 assert change_desc
2783 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2784 ref_to_push)]).splitlines()
2785 if len(commits) > 1:
2786 print('WARNING: This will upload %d commits. Run the following command '
2787 'to see which commits will be uploaded: ' % len(commits))
2788 print('git log %s..%s' % (parent, ref_to_push))
2789 print('You can also use `git squash-branch` to squash these into a '
2790 'single commit.')
2791 ask_for_data('About to upload; enter to confirm.')
2792
2793 if options.reviewers or options.tbr_owners:
2794 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2795 change)
2796
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002797 # Extra options that can be specified at push time. Doc:
2798 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2799 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002800 if change_desc.get_reviewers(tbr_only=True):
2801 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2802 refspec_opts.append('l=Code-Review+1')
2803
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002804 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002805 if not re.match(r'^[\w ]+$', options.title):
2806 options.title = re.sub(r'[^\w ]', '', options.title)
2807 print('WARNING: Patchset title may only contain alphanumeric chars '
2808 'and spaces. Cleaned up title:\n%s' % options.title)
2809 if not options.force:
2810 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002811 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2812 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2814
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002815 if options.send_mail:
2816 if not change_desc.get_reviewers():
2817 DieWithError('Must specify reviewers to send email.')
2818 refspec_opts.append('notify=ALL')
2819 else:
2820 refspec_opts.append('notify=NONE')
2821
tandrii99a72f22016-08-17 14:33:24 -07002822 reviewers = change_desc.get_reviewers()
2823 if reviewers:
2824 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002825
agablec6787972016-09-09 16:13:34 -07002826 if options.private:
2827 refspec_opts.append('draft')
2828
rmistry9eadede2016-09-19 11:22:43 -07002829 if options.topic:
2830 # Documentation on Gerrit topics is here:
2831 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2832 refspec_opts.append('topic=%s' % options.topic)
2833
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002834 refspec_suffix = ''
2835 if refspec_opts:
2836 refspec_suffix = '%' + ','.join(refspec_opts)
2837 assert ' ' not in refspec_suffix, (
2838 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002839 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002840
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002841 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002842 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002843 print_stdout=True,
2844 # Flush after every line: useful for seeing progress when running as
2845 # recipe.
2846 filter_fn=lambda _: sys.stdout.flush())
2847
2848 if options.squash:
2849 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2850 change_numbers = [m.group(1)
2851 for m in map(regex.match, push_stdout.splitlines())
2852 if m]
2853 if len(change_numbers) != 1:
2854 DieWithError(
2855 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2856 'Change-Id: %s') % (len(change_numbers), change_id))
2857 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002858 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002859
2860 # Add cc's from the CC_LIST and --cc flag (if any).
2861 cc = self.GetCCList().split(',')
2862 if options.cc:
2863 cc.extend(options.cc)
2864 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002865 if change_desc.get_cced():
2866 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002867 if cc:
2868 gerrit_util.AddReviewers(
2869 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2870
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002871 return 0
2872
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002873 def _AddChangeIdToCommitMessage(self, options, args):
2874 """Re-commits using the current message, assumes the commit hook is in
2875 place.
2876 """
2877 log_desc = options.message or CreateDescriptionFromLog(args)
2878 git_command = ['commit', '--amend', '-m', log_desc]
2879 RunGit(git_command)
2880 new_log_desc = CreateDescriptionFromLog(args)
2881 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002882 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002883 return new_log_desc
2884 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002885 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002887 def SetCQState(self, new_state):
2888 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002889 vote_map = {
2890 _CQState.NONE: 0,
2891 _CQState.DRY_RUN: 1,
2892 _CQState.COMMIT : 2,
2893 }
2894 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2895 labels={'Commit-Queue': vote_map[new_state]})
2896
tandriie113dfd2016-10-11 10:20:12 -07002897 def CannotTriggerTryJobReason(self):
2898 # TODO(tandrii): implement for Gerrit.
2899 raise NotImplementedError()
2900
tandriide281ae2016-10-12 06:02:30 -07002901 def GetIssueOwner(self):
2902 # TODO(tandrii): implement for Gerrit.
2903 raise NotImplementedError()
2904
2905 def GetIssueProject(self):
2906 # TODO(tandrii): implement for Gerrit.
2907 raise NotImplementedError()
2908
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002909
2910_CODEREVIEW_IMPLEMENTATIONS = {
2911 'rietveld': _RietveldChangelistImpl,
2912 'gerrit': _GerritChangelistImpl,
2913}
2914
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002915
iannuccie53c9352016-08-17 14:40:40 -07002916def _add_codereview_issue_select_options(parser, extra=""):
2917 _add_codereview_select_options(parser)
2918
2919 text = ('Operate on this issue number instead of the current branch\'s '
2920 'implicit issue.')
2921 if extra:
2922 text += ' '+extra
2923 parser.add_option('-i', '--issue', type=int, help=text)
2924
2925
2926def _process_codereview_issue_select_options(parser, options):
2927 _process_codereview_select_options(parser, options)
2928 if options.issue is not None and not options.forced_codereview:
2929 parser.error('--issue must be specified with either --rietveld or --gerrit')
2930
2931
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002932def _add_codereview_select_options(parser):
2933 """Appends --gerrit and --rietveld options to force specific codereview."""
2934 parser.codereview_group = optparse.OptionGroup(
2935 parser, 'EXPERIMENTAL! Codereview override options')
2936 parser.add_option_group(parser.codereview_group)
2937 parser.codereview_group.add_option(
2938 '--gerrit', action='store_true',
2939 help='Force the use of Gerrit for codereview')
2940 parser.codereview_group.add_option(
2941 '--rietveld', action='store_true',
2942 help='Force the use of Rietveld for codereview')
2943
2944
2945def _process_codereview_select_options(parser, options):
2946 if options.gerrit and options.rietveld:
2947 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2948 options.forced_codereview = None
2949 if options.gerrit:
2950 options.forced_codereview = 'gerrit'
2951 elif options.rietveld:
2952 options.forced_codereview = 'rietveld'
2953
2954
tandriif9aefb72016-07-01 09:06:51 -07002955def _get_bug_line_values(default_project, bugs):
2956 """Given default_project and comma separated list of bugs, yields bug line
2957 values.
2958
2959 Each bug can be either:
2960 * a number, which is combined with default_project
2961 * string, which is left as is.
2962
2963 This function may produce more than one line, because bugdroid expects one
2964 project per line.
2965
2966 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2967 ['v8:123', 'chromium:789']
2968 """
2969 default_bugs = []
2970 others = []
2971 for bug in bugs.split(','):
2972 bug = bug.strip()
2973 if bug:
2974 try:
2975 default_bugs.append(int(bug))
2976 except ValueError:
2977 others.append(bug)
2978
2979 if default_bugs:
2980 default_bugs = ','.join(map(str, default_bugs))
2981 if default_project:
2982 yield '%s:%s' % (default_project, default_bugs)
2983 else:
2984 yield default_bugs
2985 for other in sorted(others):
2986 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2987 yield other
2988
2989
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002990class ChangeDescription(object):
2991 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002992 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002993 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002994 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002995
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002996 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002997 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002998
agable@chromium.org42c20792013-09-12 17:34:49 +00002999 @property # www.logilab.org/ticket/89786
3000 def description(self): # pylint: disable=E0202
3001 return '\n'.join(self._description_lines)
3002
3003 def set_description(self, desc):
3004 if isinstance(desc, basestring):
3005 lines = desc.splitlines()
3006 else:
3007 lines = [line.rstrip() for line in desc]
3008 while lines and not lines[0]:
3009 lines.pop(0)
3010 while lines and not lines[-1]:
3011 lines.pop(-1)
3012 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003013
piman@chromium.org336f9122014-09-04 02:16:55 +00003014 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003015 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003016 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003017 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003018 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003019 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020
agable@chromium.org42c20792013-09-12 17:34:49 +00003021 # Get the set of R= and TBR= lines and remove them from the desciption.
3022 regexp = re.compile(self.R_LINE)
3023 matches = [regexp.match(line) for line in self._description_lines]
3024 new_desc = [l for i, l in enumerate(self._description_lines)
3025 if not matches[i]]
3026 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 # Construct new unified R= and TBR= lines.
3029 r_names = []
3030 tbr_names = []
3031 for match in matches:
3032 if not match:
3033 continue
3034 people = cleanup_list([match.group(2).strip()])
3035 if match.group(1) == 'TBR':
3036 tbr_names.extend(people)
3037 else:
3038 r_names.extend(people)
3039 for name in r_names:
3040 if name not in reviewers:
3041 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003042 if add_owners_tbr:
3043 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003044 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003045 all_reviewers = set(tbr_names + reviewers)
3046 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3047 all_reviewers)
3048 tbr_names.extend(owners_db.reviewers_for(missing_files,
3049 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003050 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3051 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3052
3053 # Put the new lines in the description where the old first R= line was.
3054 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3055 if 0 <= line_loc < len(self._description_lines):
3056 if new_tbr_line:
3057 self._description_lines.insert(line_loc, new_tbr_line)
3058 if new_r_line:
3059 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003060 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003061 if new_r_line:
3062 self.append_footer(new_r_line)
3063 if new_tbr_line:
3064 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003065
tandriif9aefb72016-07-01 09:06:51 -07003066 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003067 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003068 self.set_description([
3069 '# Enter a description of the change.',
3070 '# This will be displayed on the codereview site.',
3071 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003072 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003073 '--------------------',
3074 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 regexp = re.compile(self.BUG_LINE)
3077 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003078 prefix = settings.GetBugPrefix()
3079 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3080 for value in values:
3081 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3082 self.append_footer('BUG=%s' % value)
3083
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003085 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003086 if not content:
3087 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003089
3090 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003091 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3092 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003093 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003095
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003096 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003097 """Adds a footer line to the description.
3098
3099 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3100 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3101 that Gerrit footers are always at the end.
3102 """
3103 parsed_footer_line = git_footers.parse_footer(line)
3104 if parsed_footer_line:
3105 # Line is a gerrit footer in the form: Footer-Key: any value.
3106 # Thus, must be appended observing Gerrit footer rules.
3107 self.set_description(
3108 git_footers.add_footer(self.description,
3109 key=parsed_footer_line[0],
3110 value=parsed_footer_line[1]))
3111 return
3112
3113 if not self._description_lines:
3114 self._description_lines.append(line)
3115 return
3116
3117 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3118 if gerrit_footers:
3119 # git_footers.split_footers ensures that there is an empty line before
3120 # actual (gerrit) footers, if any. We have to keep it that way.
3121 assert top_lines and top_lines[-1] == ''
3122 top_lines, separator = top_lines[:-1], top_lines[-1:]
3123 else:
3124 separator = [] # No need for separator if there are no gerrit_footers.
3125
3126 prev_line = top_lines[-1] if top_lines else ''
3127 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3128 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3129 top_lines.append('')
3130 top_lines.append(line)
3131 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003132
tandrii99a72f22016-08-17 14:33:24 -07003133 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003134 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003135 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003136 reviewers = [match.group(2).strip()
3137 for match in matches
3138 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003139 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003140
bradnelsond975b302016-10-23 12:20:23 -07003141 def get_cced(self):
3142 """Retrieves the list of reviewers."""
3143 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3144 cced = [match.group(2).strip() for match in matches if match]
3145 return cleanup_list(cced)
3146
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003147
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003148def get_approving_reviewers(props):
3149 """Retrieves the reviewers that approved a CL from the issue properties with
3150 messages.
3151
3152 Note that the list may contain reviewers that are not committer, thus are not
3153 considered by the CQ.
3154 """
3155 return sorted(
3156 set(
3157 message['sender']
3158 for message in props['messages']
3159 if message['approval'] and message['sender'] in props['reviewers']
3160 )
3161 )
3162
3163
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003164def FindCodereviewSettingsFile(filename='codereview.settings'):
3165 """Finds the given file starting in the cwd and going up.
3166
3167 Only looks up to the top of the repository unless an
3168 'inherit-review-settings-ok' file exists in the root of the repository.
3169 """
3170 inherit_ok_file = 'inherit-review-settings-ok'
3171 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003172 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3174 root = '/'
3175 while True:
3176 if filename in os.listdir(cwd):
3177 if os.path.isfile(os.path.join(cwd, filename)):
3178 return open(os.path.join(cwd, filename))
3179 if cwd == root:
3180 break
3181 cwd = os.path.dirname(cwd)
3182
3183
3184def LoadCodereviewSettingsFromFile(fileobj):
3185 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003186 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003188 def SetProperty(name, setting, unset_error_ok=False):
3189 fullname = 'rietveld.' + name
3190 if setting in keyvals:
3191 RunGit(['config', fullname, keyvals[setting]])
3192 else:
3193 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3194
tandrii48df5812016-10-17 03:55:37 -07003195 if not keyvals.get('GERRIT_HOST', False):
3196 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003197 # Only server setting is required. Other settings can be absent.
3198 # In that case, we ignore errors raised during option deletion attempt.
3199 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003200 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3202 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003203 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003204 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003205 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3206 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003207 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003208 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003209 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003210 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003211 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3212 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003214 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003215 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003216
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003217 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003218 RunGit(['config', 'gerrit.squash-uploads',
3219 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003220
tandrii@chromium.org28253532016-04-14 13:46:56 +00003221 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003222 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003223 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003225 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3226 #should be of the form
3227 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3228 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3229 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3230 keyvals['ORIGIN_URL_CONFIG']])
3231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003233def urlretrieve(source, destination):
3234 """urllib is broken for SSL connections via a proxy therefore we
3235 can't use urllib.urlretrieve()."""
3236 with open(destination, 'w') as f:
3237 f.write(urllib2.urlopen(source).read())
3238
3239
ukai@chromium.org712d6102013-11-27 00:52:58 +00003240def hasSheBang(fname):
3241 """Checks fname is a #! script."""
3242 with open(fname) as f:
3243 return f.read(2).startswith('#!')
3244
3245
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003246# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3247def DownloadHooks(*args, **kwargs):
3248 pass
3249
3250
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003251def DownloadGerritHook(force):
3252 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003253
3254 Args:
3255 force: True to update hooks. False to install hooks if not present.
3256 """
3257 if not settings.GetIsGerrit():
3258 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003259 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003260 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3261 if not os.access(dst, os.X_OK):
3262 if os.path.exists(dst):
3263 if not force:
3264 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003265 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003266 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003267 if not hasSheBang(dst):
3268 DieWithError('Not a script: %s\n'
3269 'You need to download from\n%s\n'
3270 'into .git/hooks/commit-msg and '
3271 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003272 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3273 except Exception:
3274 if os.path.exists(dst):
3275 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003276 DieWithError('\nFailed to download hooks.\n'
3277 'You need to download from\n%s\n'
3278 'into .git/hooks/commit-msg and '
3279 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003280
3281
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003282
3283def GetRietveldCodereviewSettingsInteractively():
3284 """Prompt the user for settings."""
3285 server = settings.GetDefaultServerUrl(error_ok=True)
3286 prompt = 'Rietveld server (host[:port])'
3287 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3288 newserver = ask_for_data(prompt + ':')
3289 if not server and not newserver:
3290 newserver = DEFAULT_SERVER
3291 if newserver:
3292 newserver = gclient_utils.UpgradeToHttps(newserver)
3293 if newserver != server:
3294 RunGit(['config', 'rietveld.server', newserver])
3295
3296 def SetProperty(initial, caption, name, is_url):
3297 prompt = caption
3298 if initial:
3299 prompt += ' ("x" to clear) [%s]' % initial
3300 new_val = ask_for_data(prompt + ':')
3301 if new_val == 'x':
3302 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3303 elif new_val:
3304 if is_url:
3305 new_val = gclient_utils.UpgradeToHttps(new_val)
3306 if new_val != initial:
3307 RunGit(['config', 'rietveld.' + name, new_val])
3308
3309 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3310 SetProperty(settings.GetDefaultPrivateFlag(),
3311 'Private flag (rietveld only)', 'private', False)
3312 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3313 'tree-status-url', False)
3314 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3315 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3316 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3317 'run-post-upload-hook', False)
3318
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003319@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003321 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003322
tandrii5d0a0422016-09-14 06:24:35 -07003323 print('WARNING: git cl config works for Rietveld only')
3324 # TODO(tandrii): remove this once we switch to Gerrit.
3325 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003326 parser.add_option('--activate-update', action='store_true',
3327 help='activate auto-updating [rietveld] section in '
3328 '.git/config')
3329 parser.add_option('--deactivate-update', action='store_true',
3330 help='deactivate auto-updating [rietveld] section in '
3331 '.git/config')
3332 options, args = parser.parse_args(args)
3333
3334 if options.deactivate_update:
3335 RunGit(['config', 'rietveld.autoupdate', 'false'])
3336 return
3337
3338 if options.activate_update:
3339 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3340 return
3341
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003342 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003343 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003344 return 0
3345
3346 url = args[0]
3347 if not url.endswith('codereview.settings'):
3348 url = os.path.join(url, 'codereview.settings')
3349
3350 # Load code review settings and download hooks (if available).
3351 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3352 return 0
3353
3354
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003355def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003356 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003357 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3358 branch = ShortBranchName(branchref)
3359 _, args = parser.parse_args(args)
3360 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003361 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003362 return RunGit(['config', 'branch.%s.base-url' % branch],
3363 error_ok=False).strip()
3364 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003365 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003366 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3367 error_ok=False).strip()
3368
3369
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003370def color_for_status(status):
3371 """Maps a Changelist status to color, for CMDstatus and other tools."""
3372 return {
3373 'unsent': Fore.RED,
3374 'waiting': Fore.BLUE,
3375 'reply': Fore.YELLOW,
3376 'lgtm': Fore.GREEN,
3377 'commit': Fore.MAGENTA,
3378 'closed': Fore.CYAN,
3379 'error': Fore.WHITE,
3380 }.get(status, Fore.WHITE)
3381
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003382
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003383def get_cl_statuses(changes, fine_grained, max_processes=None):
3384 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003385
3386 If fine_grained is true, this will fetch CL statuses from the server.
3387 Otherwise, simply indicate if there's a matching url for the given branches.
3388
3389 If max_processes is specified, it is used as the maximum number of processes
3390 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3391 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003392
3393 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003394 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003395 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003396 upload.verbosity = 0
3397
3398 if fine_grained:
3399 # Process one branch synchronously to work through authentication, then
3400 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003401 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003402 def fetch(cl):
3403 try:
3404 return (cl, cl.GetStatus())
3405 except:
3406 # See http://crbug.com/629863.
3407 logging.exception('failed to fetch status for %s:', cl)
3408 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003409 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003410
tandriiea9514a2016-08-17 12:32:37 -07003411 changes_to_fetch = changes[1:]
3412 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003413 # Exit early if there was only one branch to fetch.
3414 return
3415
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003416 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003417 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003418 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003419 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003420
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003421 fetched_cls = set()
3422 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003423 while True:
3424 try:
3425 row = it.next(timeout=5)
3426 except multiprocessing.TimeoutError:
3427 break
3428
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003429 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003430 yield row
3431
3432 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003433 for cl in set(changes_to_fetch) - fetched_cls:
3434 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003435
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003436 else:
3437 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003438 for cl in changes:
3439 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003440
rmistry@google.com2dd99862015-06-22 12:22:18 +00003441
3442def upload_branch_deps(cl, args):
3443 """Uploads CLs of local branches that are dependents of the current branch.
3444
3445 If the local branch dependency tree looks like:
3446 test1 -> test2.1 -> test3.1
3447 -> test3.2
3448 -> test2.2 -> test3.3
3449
3450 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3451 run on the dependent branches in this order:
3452 test2.1, test3.1, test3.2, test2.2, test3.3
3453
3454 Note: This function does not rebase your local dependent branches. Use it when
3455 you make a change to the parent branch that will not conflict with its
3456 dependent branches, and you would like their dependencies updated in
3457 Rietveld.
3458 """
3459 if git_common.is_dirty_git_tree('upload-branch-deps'):
3460 return 1
3461
3462 root_branch = cl.GetBranch()
3463 if root_branch is None:
3464 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3465 'Get on a branch!')
3466 if not cl.GetIssue() or not cl.GetPatchset():
3467 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3468 'patchset dependencies without an uploaded CL.')
3469
3470 branches = RunGit(['for-each-ref',
3471 '--format=%(refname:short) %(upstream:short)',
3472 'refs/heads'])
3473 if not branches:
3474 print('No local branches found.')
3475 return 0
3476
3477 # Create a dictionary of all local branches to the branches that are dependent
3478 # on it.
3479 tracked_to_dependents = collections.defaultdict(list)
3480 for b in branches.splitlines():
3481 tokens = b.split()
3482 if len(tokens) == 2:
3483 branch_name, tracked = tokens
3484 tracked_to_dependents[tracked].append(branch_name)
3485
vapiera7fbd5a2016-06-16 09:17:49 -07003486 print()
3487 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003488 dependents = []
3489 def traverse_dependents_preorder(branch, padding=''):
3490 dependents_to_process = tracked_to_dependents.get(branch, [])
3491 padding += ' '
3492 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003493 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003494 dependents.append(dependent)
3495 traverse_dependents_preorder(dependent, padding)
3496 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003497 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003498
3499 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003500 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003501 return 0
3502
vapiera7fbd5a2016-06-16 09:17:49 -07003503 print('This command will checkout all dependent branches and run '
3504 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003505 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3506
andybons@chromium.org962f9462016-02-03 20:00:42 +00003507 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003508 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003509 args.extend(['-t', 'Updated patchset dependency'])
3510
rmistry@google.com2dd99862015-06-22 12:22:18 +00003511 # Record all dependents that failed to upload.
3512 failures = {}
3513 # Go through all dependents, checkout the branch and upload.
3514 try:
3515 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003516 print()
3517 print('--------------------------------------')
3518 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003519 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003520 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003521 try:
3522 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003523 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003524 failures[dependent_branch] = 1
3525 except: # pylint: disable=W0702
3526 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003527 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003528 finally:
3529 # Swap back to the original root branch.
3530 RunGit(['checkout', '-q', root_branch])
3531
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print()
3533 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003534 for dependent_branch in dependents:
3535 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003536 print(' %s : %s' % (dependent_branch, upload_status))
3537 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003538
3539 return 0
3540
3541
kmarshall3bff56b2016-06-06 18:31:47 -07003542def CMDarchive(parser, args):
3543 """Archives and deletes branches associated with closed changelists."""
3544 parser.add_option(
3545 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003546 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003547 parser.add_option(
3548 '-f', '--force', action='store_true',
3549 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003550 parser.add_option(
3551 '-d', '--dry-run', action='store_true',
3552 help='Skip the branch tagging and removal steps.')
3553 parser.add_option(
3554 '-t', '--notags', action='store_true',
3555 help='Do not tag archived branches. '
3556 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003557
3558 auth.add_auth_options(parser)
3559 options, args = parser.parse_args(args)
3560 if args:
3561 parser.error('Unsupported args: %s' % ' '.join(args))
3562 auth_config = auth.extract_auth_config_from_options(options)
3563
3564 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3565 if not branches:
3566 return 0
3567
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003569 changes = [Changelist(branchref=b, auth_config=auth_config)
3570 for b in branches.splitlines()]
3571 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3572 statuses = get_cl_statuses(changes,
3573 fine_grained=True,
3574 max_processes=options.maxjobs)
3575 proposal = [(cl.GetBranch(),
3576 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3577 for cl, status in statuses
3578 if status == 'closed']
3579 proposal.sort()
3580
3581 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003582 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003583 return 0
3584
3585 current_branch = GetCurrentBranch()
3586
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003588 if options.notags:
3589 for next_item in proposal:
3590 print(' ' + next_item[0])
3591 else:
3592 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3593 for next_item in proposal:
3594 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003595
kmarshall9249e012016-08-23 12:02:16 -07003596 # Quit now on precondition failure or if instructed by the user, either
3597 # via an interactive prompt or by command line flags.
3598 if options.dry_run:
3599 print('\nNo changes were made (dry run).\n')
3600 return 0
3601 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003602 print('You are currently on a branch \'%s\' which is associated with a '
3603 'closed codereview issue, so archive cannot proceed. Please '
3604 'checkout another branch and run this command again.' %
3605 current_branch)
3606 return 1
kmarshall9249e012016-08-23 12:02:16 -07003607 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003608 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3609 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003610 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003611 return 1
3612
3613 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003614 if not options.notags:
3615 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003616 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003617
vapiera7fbd5a2016-06-16 09:17:49 -07003618 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003619
3620 return 0
3621
3622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003624 """Show status of changelists.
3625
3626 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003627 - Red not sent for review or broken
3628 - Blue waiting for review
3629 - Yellow waiting for you to reply to review
3630 - Green LGTM'ed
3631 - Magenta in the commit queue
3632 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003633
3634 Also see 'git cl comments'.
3635 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003637 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003638 parser.add_option('-f', '--fast', action='store_true',
3639 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003640 parser.add_option(
3641 '-j', '--maxjobs', action='store', type=int,
3642 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003643
3644 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003645 _add_codereview_issue_select_options(
3646 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003647 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003648 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003649 if args:
3650 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003651 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003652
iannuccie53c9352016-08-17 14:40:40 -07003653 if options.issue is not None and not options.field:
3654 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003657 cl = Changelist(auth_config=auth_config, issue=options.issue,
3658 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003660 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661 elif options.field == 'id':
3662 issueid = cl.GetIssue()
3663 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 elif options.field == 'patch':
3666 patchset = cl.GetPatchset()
3667 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003668 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003669 elif options.field == 'status':
3670 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003671 elif options.field == 'url':
3672 url = cl.GetIssueURL()
3673 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003675 return 0
3676
3677 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3678 if not branches:
3679 print('No local branch found.')
3680 return 0
3681
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003682 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003683 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003684 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003685 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003686 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003687 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003688 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003689
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003690 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003691 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3692 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3693 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003694 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003695 c, status = output.next()
3696 branch_statuses[c.GetBranch()] = status
3697 status = branch_statuses.pop(branch)
3698 url = cl.GetIssueURL()
3699 if url and (not status or status == 'error'):
3700 # The issue probably doesn't exist anymore.
3701 url += ' (broken)'
3702
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003703 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003704 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003705 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003706 color = ''
3707 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003708 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003709 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003710 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003711 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003712
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003713 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003714 print()
3715 print('Current branch:',)
3716 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003717 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003719 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003720 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003721 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print('Issue description:')
3723 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003724 return 0
3725
3726
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003727def colorize_CMDstatus_doc():
3728 """To be called once in main() to add colors to git cl status help."""
3729 colors = [i for i in dir(Fore) if i[0].isupper()]
3730
3731 def colorize_line(line):
3732 for color in colors:
3733 if color in line.upper():
3734 # Extract whitespaces first and the leading '-'.
3735 indent = len(line) - len(line.lstrip(' ')) + 1
3736 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3737 return line
3738
3739 lines = CMDstatus.__doc__.splitlines()
3740 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3741
3742
phajdan.jre328cf92016-08-22 04:12:17 -07003743def write_json(path, contents):
3744 with open(path, 'w') as f:
3745 json.dump(contents, f)
3746
3747
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003748@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003749def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003750 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751
3752 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003753 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003754 parser.add_option('-r', '--reverse', action='store_true',
3755 help='Lookup the branch(es) for the specified issues. If '
3756 'no issues are specified, all branches with mapped '
3757 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003758 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003759 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003760 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003761 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003762
dnj@chromium.org406c4402015-03-03 17:22:28 +00003763 if options.reverse:
3764 branches = RunGit(['for-each-ref', 'refs/heads',
3765 '--format=%(refname:short)']).splitlines()
3766
3767 # Reverse issue lookup.
3768 issue_branch_map = {}
3769 for branch in branches:
3770 cl = Changelist(branchref=branch)
3771 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3772 if not args:
3773 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003774 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003775 for issue in args:
3776 if not issue:
3777 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003778 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print('Branch for issue number %s: %s' % (
3780 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003781 if options.json:
3782 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003783 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003784 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003785 if len(args) > 0:
3786 try:
3787 issue = int(args[0])
3788 except ValueError:
3789 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003790 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003791 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003792 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003793 if options.json:
3794 write_json(options.json, {
3795 'issue': cl.GetIssue(),
3796 'issue_url': cl.GetIssueURL(),
3797 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003798 return 0
3799
3800
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003801def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003802 """Shows or posts review comments for any changelist."""
3803 parser.add_option('-a', '--add-comment', dest='comment',
3804 help='comment to add to an issue')
3805 parser.add_option('-i', dest='issue',
3806 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003807 parser.add_option('-j', '--json-file',
3808 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003809 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003810 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003811 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003812
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003813 issue = None
3814 if options.issue:
3815 try:
3816 issue = int(options.issue)
3817 except ValueError:
3818 DieWithError('A review issue id is expected to be a number')
3819
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003820 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003821
3822 if options.comment:
3823 cl.AddComment(options.comment)
3824 return 0
3825
3826 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003827 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003828 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003829 summary.append({
3830 'date': message['date'],
3831 'lgtm': False,
3832 'message': message['text'],
3833 'not_lgtm': False,
3834 'sender': message['sender'],
3835 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003836 if message['disapproval']:
3837 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003838 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003839 elif message['approval']:
3840 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003841 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003842 elif message['sender'] == data['owner_email']:
3843 color = Fore.MAGENTA
3844 else:
3845 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003847 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003848 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003849 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003851 if options.json_file:
3852 with open(options.json_file, 'wb') as f:
3853 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003854 return 0
3855
3856
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003857@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003858def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003859 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003860 parser.add_option('-d', '--display', action='store_true',
3861 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003862 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003863 help='New description to set for this issue (- for stdin, '
3864 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003865 parser.add_option('-f', '--force', action='store_true',
3866 help='Delete any unpublished Gerrit edits for this issue '
3867 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003868
3869 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003870 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003871 options, args = parser.parse_args(args)
3872 _process_codereview_select_options(parser, options)
3873
3874 target_issue = None
3875 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003876 target_issue = ParseIssueNumberArgument(args[0])
3877 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003878 parser.print_help()
3879 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003880
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003881 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003882
martiniss6eda05f2016-06-30 10:18:35 -07003883 kwargs = {
3884 'auth_config': auth_config,
3885 'codereview': options.forced_codereview,
3886 }
3887 if target_issue:
3888 kwargs['issue'] = target_issue.issue
3889 if options.forced_codereview == 'rietveld':
3890 kwargs['rietveld_server'] = target_issue.hostname
3891
3892 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003893
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003894 if not cl.GetIssue():
3895 DieWithError('This branch has no associated changelist.')
3896 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003897
smut@google.com34fb6b12015-07-13 20:03:26 +00003898 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003899 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003900 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003901
3902 if options.new_description:
3903 text = options.new_description
3904 if text == '-':
3905 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003906 elif text == '+':
3907 base_branch = cl.GetCommonAncestorWithUpstream()
3908 change = cl.GetChange(base_branch, None, local_description=True)
3909 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003910
3911 description.set_description(text)
3912 else:
3913 description.prompt()
3914
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003915 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003916 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003917 return 0
3918
3919
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920def CreateDescriptionFromLog(args):
3921 """Pulls out the commit log to use as a base for the CL description."""
3922 log_args = []
3923 if len(args) == 1 and not args[0].endswith('.'):
3924 log_args = [args[0] + '..']
3925 elif len(args) == 1 and args[0].endswith('...'):
3926 log_args = [args[0][:-1]]
3927 elif len(args) == 2:
3928 log_args = [args[0] + '..' + args[1]]
3929 else:
3930 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003931 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932
3933
thestig@chromium.org44202a22014-03-11 19:22:18 +00003934def CMDlint(parser, args):
3935 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003936 parser.add_option('--filter', action='append', metavar='-x,+y',
3937 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003938 auth.add_auth_options(parser)
3939 options, args = parser.parse_args(args)
3940 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003941
3942 # Access to a protected member _XX of a client class
3943 # pylint: disable=W0212
3944 try:
3945 import cpplint
3946 import cpplint_chromium
3947 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003948 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003949 return 1
3950
3951 # Change the current working directory before calling lint so that it
3952 # shows the correct base.
3953 previous_cwd = os.getcwd()
3954 os.chdir(settings.GetRoot())
3955 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003956 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003957 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3958 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003959 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003960 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003961 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003962
3963 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003964 command = args + files
3965 if options.filter:
3966 command = ['--filter=' + ','.join(options.filter)] + command
3967 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003968
3969 white_regex = re.compile(settings.GetLintRegex())
3970 black_regex = re.compile(settings.GetLintIgnoreRegex())
3971 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3972 for filename in filenames:
3973 if white_regex.match(filename):
3974 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003975 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003976 else:
3977 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3978 extra_check_functions)
3979 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003980 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003981 finally:
3982 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003983 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003984 if cpplint._cpplint_state.error_count != 0:
3985 return 1
3986 return 0
3987
3988
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003990 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003991 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003993 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003994 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003995 auth.add_auth_options(parser)
3996 options, args = parser.parse_args(args)
3997 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998
sbc@chromium.org71437c02015-04-09 19:29:40 +00003999 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004000 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001 return 1
4002
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004003 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004 if args:
4005 base_branch = args[0]
4006 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004007 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004008 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004009
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004010 cl.RunHook(
4011 committing=not options.upload,
4012 may_prompt=False,
4013 verbose=options.verbose,
4014 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004015 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016
4017
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004018def GenerateGerritChangeId(message):
4019 """Returns Ixxxxxx...xxx change id.
4020
4021 Works the same way as
4022 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4023 but can be called on demand on all platforms.
4024
4025 The basic idea is to generate git hash of a state of the tree, original commit
4026 message, author/committer info and timestamps.
4027 """
4028 lines = []
4029 tree_hash = RunGitSilent(['write-tree'])
4030 lines.append('tree %s' % tree_hash.strip())
4031 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4032 if code == 0:
4033 lines.append('parent %s' % parent.strip())
4034 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4035 lines.append('author %s' % author.strip())
4036 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4037 lines.append('committer %s' % committer.strip())
4038 lines.append('')
4039 # Note: Gerrit's commit-hook actually cleans message of some lines and
4040 # whitespace. This code is not doing this, but it clearly won't decrease
4041 # entropy.
4042 lines.append(message)
4043 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4044 stdin='\n'.join(lines))
4045 return 'I%s' % change_hash.strip()
4046
4047
wittman@chromium.org455dc922015-01-26 20:15:50 +00004048def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4049 """Computes the remote branch ref to use for the CL.
4050
4051 Args:
4052 remote (str): The git remote for the CL.
4053 remote_branch (str): The git remote branch for the CL.
4054 target_branch (str): The target branch specified by the user.
4055 pending_prefix (str): The pending prefix from the settings.
4056 """
4057 if not (remote and remote_branch):
4058 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004059
wittman@chromium.org455dc922015-01-26 20:15:50 +00004060 if target_branch:
4061 # Cannonicalize branch references to the equivalent local full symbolic
4062 # refs, which are then translated into the remote full symbolic refs
4063 # below.
4064 if '/' not in target_branch:
4065 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4066 else:
4067 prefix_replacements = (
4068 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4069 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4070 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4071 )
4072 match = None
4073 for regex, replacement in prefix_replacements:
4074 match = re.search(regex, target_branch)
4075 if match:
4076 remote_branch = target_branch.replace(match.group(0), replacement)
4077 break
4078 if not match:
4079 # This is a branch path but not one we recognize; use as-is.
4080 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004081 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4082 # Handle the refs that need to land in different refs.
4083 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004084
wittman@chromium.org455dc922015-01-26 20:15:50 +00004085 # Create the true path to the remote branch.
4086 # Does the following translation:
4087 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4088 # * refs/remotes/origin/master -> refs/heads/master
4089 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4090 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4091 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4092 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4093 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4094 'refs/heads/')
4095 elif remote_branch.startswith('refs/remotes/branch-heads'):
4096 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4097 # If a pending prefix exists then replace refs/ with it.
4098 if pending_prefix:
4099 remote_branch = remote_branch.replace('refs/', pending_prefix)
4100 return remote_branch
4101
4102
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004103def cleanup_list(l):
4104 """Fixes a list so that comma separated items are put as individual items.
4105
4106 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4107 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4108 """
4109 items = sum((i.split(',') for i in l), [])
4110 stripped_items = (i.strip() for i in items)
4111 return sorted(filter(None, stripped_items))
4112
4113
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004114@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004115def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004116 """Uploads the current changelist to codereview.
4117
4118 Can skip dependency patchset uploads for a branch by running:
4119 git config branch.branch_name.skip-deps-uploads True
4120 To unset run:
4121 git config --unset branch.branch_name.skip-deps-uploads
4122 Can also set the above globally by using the --global flag.
4123 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004124 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4125 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004126 parser.add_option('--bypass-watchlists', action='store_true',
4127 dest='bypass_watchlists',
4128 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004129 parser.add_option('-f', action='store_true', dest='force',
4130 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004131 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004132 parser.add_option('-b', '--bug',
4133 help='pre-populate the bug number(s) for this issue. '
4134 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004135 parser.add_option('--message-file', dest='message_file',
4136 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004137 parser.add_option('-t', dest='title',
4138 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004139 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004140 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004141 help='reviewer email addresses')
4142 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004143 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004144 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004145 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004146 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004147 parser.add_option('--emulate_svn_auto_props',
4148 '--emulate-svn-auto-props',
4149 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004150 dest="emulate_svn_auto_props",
4151 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004152 parser.add_option('-c', '--use-commit-queue', action='store_true',
4153 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004154 parser.add_option('--private', action='store_true',
4155 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004156 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004157 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004158 metavar='TARGET',
4159 help='Apply CL to remote ref TARGET. ' +
4160 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004161 parser.add_option('--squash', action='store_true',
4162 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004163 parser.add_option('--no-squash', action='store_true',
4164 help='Don\'t squash multiple commits into one ' +
4165 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004166 parser.add_option('--topic', default=None,
4167 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004168 parser.add_option('--email', default=None,
4169 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004170 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4171 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004172 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4173 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004174 help='Send the patchset to do a CQ dry run right after '
4175 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004176 parser.add_option('--dependencies', action='store_true',
4177 help='Uploads CLs of all the local branches that depend on '
4178 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004179
rmistry@google.com2dd99862015-06-22 12:22:18 +00004180 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004181 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004182 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004183 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004184 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004185 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004186 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004187
sbc@chromium.org71437c02015-04-09 19:29:40 +00004188 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004189 return 1
4190
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004191 options.reviewers = cleanup_list(options.reviewers)
4192 options.cc = cleanup_list(options.cc)
4193
tandriib80458a2016-06-23 12:20:07 -07004194 if options.message_file:
4195 if options.message:
4196 parser.error('only one of --message and --message-file allowed.')
4197 options.message = gclient_utils.FileRead(options.message_file)
4198 options.message_file = None
4199
tandrii4d0545a2016-07-06 03:56:49 -07004200 if options.cq_dry_run and options.use_commit_queue:
4201 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4202
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004203 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4204 settings.GetIsGerrit()
4205
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004206 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004207 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004208
4209
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004210def IsSubmoduleMergeCommit(ref):
4211 # When submodules are added to the repo, we expect there to be a single
4212 # non-git-svn merge commit at remote HEAD with a signature comment.
4213 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004214 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004215 return RunGit(cmd) != ''
4216
4217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004219 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004221 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4222 upstream and closes the issue automatically and atomically.
4223
4224 Otherwise (in case of Rietveld):
4225 Squashes branch into a single commit.
4226 Updates changelog with metadata (e.g. pointer to review).
4227 Pushes/dcommits the code upstream.
4228 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229 """
4230 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4231 help='bypass upload presubmit hook')
4232 parser.add_option('-m', dest='message',
4233 help="override review description")
4234 parser.add_option('-f', action='store_true', dest='force',
4235 help="force yes to questions (don't prompt)")
4236 parser.add_option('-c', dest='contributor',
4237 help="external contributor for patch (appended to " +
4238 "description and used as author for git). Should be " +
4239 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004240 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004241 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004242 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004243 auth_config = auth.extract_auth_config_from_options(options)
4244
4245 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004247 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4248 if cl.IsGerrit():
4249 if options.message:
4250 # This could be implemented, but it requires sending a new patch to
4251 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4252 # Besides, Gerrit has the ability to change the commit message on submit
4253 # automatically, thus there is no need to support this option (so far?).
4254 parser.error('-m MESSAGE option is not supported for Gerrit.')
4255 if options.contributor:
4256 parser.error(
4257 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4258 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4259 'the contributor\'s "name <email>". If you can\'t upload such a '
4260 'commit for review, contact your repository admin and request'
4261 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004262 if not cl.GetIssue():
4263 DieWithError('You must upload the issue first to Gerrit.\n'
4264 ' If you would rather have `git cl land` upload '
4265 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004266 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4267 options.verbose)
4268
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004269 current = cl.GetBranch()
4270 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4271 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004272 print()
4273 print('Attempting to push branch %r into another local branch!' % current)
4274 print()
4275 print('Either reparent this branch on top of origin/master:')
4276 print(' git reparent-branch --root')
4277 print()
4278 print('OR run `git rebase-update` if you think the parent branch is ')
4279 print('already committed.')
4280 print()
4281 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004282 return 1
4283
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004284 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285 # Default to merging against our best guess of the upstream branch.
4286 args = [cl.GetUpstreamBranch()]
4287
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004288 if options.contributor:
4289 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004290 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004291 return 1
4292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004294 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295
sbc@chromium.org71437c02015-04-09 19:29:40 +00004296 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004297 return 1
4298
4299 # This rev-list syntax means "show all commits not in my branch that
4300 # are in base_branch".
4301 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4302 base_branch]).splitlines()
4303 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('Base branch "%s" has %d commits '
4305 'not in this branch.' % (base_branch, len(upstream_commits)))
4306 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004307 return 1
4308
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004309 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004310 svn_head = None
4311 if cmd == 'dcommit' or base_has_submodules:
4312 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4313 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004316 # If the base_head is a submodule merge commit, the first parent of the
4317 # base_head should be a git-svn commit, which is what we're interested in.
4318 base_svn_head = base_branch
4319 if base_has_submodules:
4320 base_svn_head += '^1'
4321
4322 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004323 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004324 print('This branch has %d additional commits not upstreamed yet.'
4325 % len(extra_commits.splitlines()))
4326 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4327 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004328 return 1
4329
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004330 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004331 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004332 author = None
4333 if options.contributor:
4334 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004335 hook_results = cl.RunHook(
4336 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004337 may_prompt=not options.force,
4338 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004339 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004340 if not hook_results.should_continue():
4341 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004342
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004343 # Check the tree status if the tree status URL is set.
4344 status = GetTreeStatus()
4345 if 'closed' == status:
4346 print('The tree is closed. Please wait for it to reopen. Use '
4347 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4348 return 1
4349 elif 'unknown' == status:
4350 print('Unable to determine tree status. Please verify manually and '
4351 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4352 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004353
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004354 change_desc = ChangeDescription(options.message)
4355 if not change_desc.description and cl.GetIssue():
4356 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004358 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004359 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004360 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004361 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004362 print('No description set.')
4363 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004364 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004365
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004366 # Keep a separate copy for the commit message, because the commit message
4367 # contains the link to the Rietveld issue, while the Rietveld message contains
4368 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004369 # Keep a separate copy for the commit message.
4370 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004371 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004372
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004373 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004374 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004375 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004376 # after it. Add a period on a new line to circumvent this. Also add a space
4377 # before the period to make sure that Gitiles continues to correctly resolve
4378 # the URL.
4379 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004380 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004381 commit_desc.append_footer('Patch from %s.' % options.contributor)
4382
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004383 print('Description:')
4384 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004386 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004388 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004390 # We want to squash all this branch's commits into one commit with the proper
4391 # description. We do this by doing a "reset --soft" to the base branch (which
4392 # keeps the working copy the same), then dcommitting that. If origin/master
4393 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4394 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004396 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4397 # Delete the branches if they exist.
4398 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4399 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4400 result = RunGitWithCode(showref_cmd)
4401 if result[0] == 0:
4402 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403
4404 # We might be in a directory that's present in this branch but not in the
4405 # trunk. Move up to the top of the tree so that git commands that expect a
4406 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004407 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004408 if rel_base_path:
4409 os.chdir(rel_base_path)
4410
4411 # Stuff our change into the merge branch.
4412 # We wrap in a try...finally block so if anything goes wrong,
4413 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004414 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004415 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004416 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004417 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004419 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004420 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004421 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004422 RunGit(
4423 [
4424 'commit', '--author', options.contributor,
4425 '-m', commit_desc.description,
4426 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004428 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004429 if base_has_submodules:
4430 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4431 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4432 RunGit(['checkout', CHERRY_PICK_BRANCH])
4433 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004434 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004435 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004436 mirror = settings.GetGitMirror(remote)
4437 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004438 pending_prefix = settings.GetPendingRefPrefix()
4439 if not pending_prefix or branch.startswith(pending_prefix):
4440 # If not using refs/pending/heads/* at all, or target ref is already set
4441 # to pending, then push to the target ref directly.
4442 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004443 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004444 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004445 else:
4446 # Cherry-pick the change on top of pending ref and then push it.
4447 assert branch.startswith('refs/'), branch
4448 assert pending_prefix[-1] == '/', pending_prefix
4449 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004450 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004451 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004452 if retcode == 0:
4453 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004454 else:
4455 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004456 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004457 'svn', 'dcommit',
4458 '-C%s' % options.similarity,
4459 '--no-rebase', '--rmdir',
4460 ]
4461 if settings.GetForceHttpsCommitUrl():
4462 # Allow forcing https commit URLs for some projects that don't allow
4463 # committing to http URLs (like Google Code).
4464 remote_url = cl.GetGitSvnRemoteUrl()
4465 if urlparse.urlparse(remote_url).scheme == 'http':
4466 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004467 cmd_args.append('--commit-url=%s' % remote_url)
4468 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004469 if 'Committed r' in output:
4470 revision = re.match(
4471 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4472 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004473 finally:
4474 # And then swap back to the original branch and clean up.
4475 RunGit(['checkout', '-q', cl.GetBranch()])
4476 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004477 if base_has_submodules:
4478 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004480 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004481 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004482 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004483
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004484 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004485 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004486 try:
4487 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4488 # We set pushed_to_pending to False, since it made it all the way to the
4489 # real ref.
4490 pushed_to_pending = False
4491 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004492 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004493
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004494 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004495 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004497 if not to_pending:
4498 if viewvc_url and revision:
4499 change_desc.append_footer(
4500 'Committed: %s%s' % (viewvc_url, revision))
4501 elif revision:
4502 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004503 print('Closing issue '
4504 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004505 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004506 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004507 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004508 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004509 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004510 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004511 if options.bypass_hooks:
4512 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4513 else:
4514 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004515 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004516
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004517 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004518 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004519 print('The commit is in the pending queue (%s).' % pending_ref)
4520 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4521 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004522
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004523 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4524 if os.path.isfile(hook):
4525 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004526
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004527 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004528
4529
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004530def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004531 print()
4532 print('Waiting for commit to be landed on %s...' % real_ref)
4533 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004534 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4535 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004536 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004537
4538 loop = 0
4539 while True:
4540 sys.stdout.write('fetching (%d)... \r' % loop)
4541 sys.stdout.flush()
4542 loop += 1
4543
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004544 if mirror:
4545 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004546 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4547 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4548 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4549 for commit in commits.splitlines():
4550 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004551 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004552 return commit
4553
4554 current_rev = to_rev
4555
4556
tandriibf429402016-09-14 07:09:12 -07004557def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004558 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4559
4560 Returns:
4561 (retcode of last operation, output log of last operation).
4562 """
4563 assert pending_ref.startswith('refs/'), pending_ref
4564 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4565 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4566 code = 0
4567 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004568 max_attempts = 3
4569 attempts_left = max_attempts
4570 while attempts_left:
4571 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004572 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004573 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004574
4575 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004577 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004578 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004579 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004580 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004581 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004582 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004583 continue
4584
4585 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004586 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004587 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004588 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004589 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004590 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4591 'the following files have merge conflicts:' % pending_ref)
4592 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4593 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004594 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004595 return code, out
4596
4597 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004598 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004599 code, out = RunGitWithCode(
4600 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4601 if code == 0:
4602 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004603 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004604 return code, out
4605
vapiera7fbd5a2016-06-16 09:17:49 -07004606 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004607 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004608 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004609 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004610 print('Fatal push error. Make sure your .netrc credentials and git '
4611 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004612 return code, out
4613
vapiera7fbd5a2016-06-16 09:17:49 -07004614 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004615 return code, out
4616
4617
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004618def IsFatalPushFailure(push_stdout):
4619 """True if retrying push won't help."""
4620 return '(prohibited by Gerrit)' in push_stdout
4621
4622
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004623@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004625 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004627 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004628 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004629 message = """This repository appears to be a git-svn mirror, but we
4630don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004631 else:
4632 message = """This doesn't appear to be an SVN repository.
4633If your project has a true, writeable git repository, you probably want to run
4634'git cl land' instead.
4635If your project has a git mirror of an upstream SVN master, you probably need
4636to run 'git svn init'.
4637
4638Using the wrong command might cause your commit to appear to succeed, and the
4639review to be closed, without actually landing upstream. If you choose to
4640proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004641 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004642 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004643 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4644 'Please let us know of this project you are committing to:'
4645 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 return SendUpstream(parser, args, 'dcommit')
4647
4648
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004649@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004650def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004651 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004652 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653 print('This appears to be an SVN repository.')
4654 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004655 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004656 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004657 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004658
4659
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004660@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004661def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004662 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004663 parser.add_option('-b', dest='newbranch',
4664 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004665 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004666 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004667 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4668 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004669 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004670 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004671 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004672 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004673 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004674 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004675
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004676
4677 group = optparse.OptionGroup(
4678 parser,
4679 'Options for continuing work on the current issue uploaded from a '
4680 'different clone (e.g. different machine). Must be used independently '
4681 'from the other options. No issue number should be specified, and the '
4682 'branch must have an issue number associated with it')
4683 group.add_option('--reapply', action='store_true', dest='reapply',
4684 help='Reset the branch and reapply the issue.\n'
4685 'CAUTION: This will undo any local changes in this '
4686 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004687
4688 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004689 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004690 parser.add_option_group(group)
4691
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004692 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004693 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004695 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth_config = auth.extract_auth_config_from_options(options)
4697
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004698
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004699 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004700 if options.newbranch:
4701 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004702 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004703 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004704
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004705 cl = Changelist(auth_config=auth_config,
4706 codereview=options.forced_codereview)
4707 if not cl.GetIssue():
4708 parser.error('current branch must have an associated issue')
4709
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004710 upstream = cl.GetUpstreamBranch()
4711 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004712 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004713
4714 RunGit(['reset', '--hard', upstream])
4715 if options.pull:
4716 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004717
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004718 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4719 options.directory)
4720
4721 if len(args) != 1 or not args[0]:
4722 parser.error('Must specify issue number or url')
4723
4724 # We don't want uncommitted changes mixed up with the patch.
4725 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004726 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004727
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004728 if options.newbranch:
4729 if options.force:
4730 RunGit(['branch', '-D', options.newbranch],
4731 stderr=subprocess2.PIPE, error_ok=True)
4732 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004733 elif not GetCurrentBranch():
4734 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004735
4736 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4737
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004738 if cl.IsGerrit():
4739 if options.reject:
4740 parser.error('--reject is not supported with Gerrit codereview.')
4741 if options.nocommit:
4742 parser.error('--nocommit is not supported with Gerrit codereview.')
4743 if options.directory:
4744 parser.error('--directory is not supported with Gerrit codereview.')
4745
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004746 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004747 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004748
4749
4750def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004751 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004752 # Provide a wrapper for git svn rebase to help avoid accidental
4753 # git svn dcommit.
4754 # It's the only command that doesn't use parser at all since we just defer
4755 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004756
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004757 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004758
4759
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004760def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004761 """Fetches the tree status and returns either 'open', 'closed',
4762 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004763 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004764 if url:
4765 status = urllib2.urlopen(url).read().lower()
4766 if status.find('closed') != -1 or status == '0':
4767 return 'closed'
4768 elif status.find('open') != -1 or status == '1':
4769 return 'open'
4770 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004771 return 'unset'
4772
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004774def GetTreeStatusReason():
4775 """Fetches the tree status from a json url and returns the message
4776 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004777 url = settings.GetTreeStatusUrl()
4778 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004779 connection = urllib2.urlopen(json_url)
4780 status = json.loads(connection.read())
4781 connection.close()
4782 return status['message']
4783
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004785def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004786 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004787 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788 status = GetTreeStatus()
4789 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004790 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004791 return 2
4792
vapiera7fbd5a2016-06-16 09:17:49 -07004793 print('The tree is %s' % status)
4794 print()
4795 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004796 if status != 'open':
4797 return 1
4798 return 0
4799
4800
maruel@chromium.org15192402012-09-06 12:38:29 +00004801def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004802 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004803 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004804 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004805 '-b', '--bot', action='append',
4806 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4807 'times to specify multiple builders. ex: '
4808 '"-b win_rel -b win_layout". See '
4809 'the try server waterfall for the builders name and the tests '
4810 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004811 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004812 '-B', '--bucket', default='',
4813 help=('Buildbucket bucket to send the try requests.'))
4814 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004815 '-m', '--master', default='',
4816 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004817 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004818 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004819 help='Revision to use for the try job; default: the revision will '
4820 'be determined by the try recipe that builder runs, which usually '
4821 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004822 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004823 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004824 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004825 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '--project',
4828 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004829 'in recipe to determine to which repository or directory to '
4830 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004831 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004832 '-p', '--property', dest='properties', action='append', default=[],
4833 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004834 'key2=value2 etc. The value will be treated as '
4835 'json if decodable, or as string otherwise. '
4836 'NOTE: using this may make your try job not usable for CQ, '
4837 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004838 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004839 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4840 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004841 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004842 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004843 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004844 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004845
machenbach@chromium.org45453142015-09-15 08:45:22 +00004846 # Make sure that all properties are prop=value pairs.
4847 bad_params = [x for x in options.properties if '=' not in x]
4848 if bad_params:
4849 parser.error('Got properties with missing "=": %s' % bad_params)
4850
maruel@chromium.org15192402012-09-06 12:38:29 +00004851 if args:
4852 parser.error('Unknown arguments: %s' % args)
4853
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004854 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004855 if not cl.GetIssue():
4856 parser.error('Need to upload first')
4857
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004858 if cl.IsGerrit():
4859 parser.error(
4860 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4861 'If your project has Commit Queue, dry run is a workaround:\n'
4862 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004863
tandriie113dfd2016-10-11 10:20:12 -07004864 error_message = cl.CannotTriggerTryJobReason()
4865 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004866 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004867
borenet6c0efe62016-10-19 08:13:29 -07004868 if options.bucket and options.master:
4869 parser.error('Only one of --bucket and --master may be used.')
4870
qyearsley1fdfcb62016-10-24 13:22:03 -07004871 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004872
qyearsley1fdfcb62016-10-24 13:22:03 -07004873 if not buckets:
4874 # Default to triggering Dry Run (see http://crbug.com/625697).
4875 if options.verbose:
4876 print('git cl try with no bots now defaults to CQ Dry Run.')
4877 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004878
borenet6c0efe62016-10-19 08:13:29 -07004879 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004880 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004881 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004882 'of bot requires an initial job from a parent (usually a builder). '
4883 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004884 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004885 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004886
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004887 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004888 if patchset != cl.GetPatchset():
4889 print('Warning: Codereview server has newer patchsets (%s) than most '
4890 'recent upload from local checkout (%s). Did a previous upload '
4891 'fail?\n'
4892 'By default, git cl try uses the latest patchset from '
4893 'codereview, continuing to use patchset %s.\n' %
4894 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004895
tandrii568043b2016-10-11 07:49:18 -07004896 try:
borenet6c0efe62016-10-19 08:13:29 -07004897 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4898 patchset)
tandrii568043b2016-10-11 07:49:18 -07004899 except BuildbucketResponseException as ex:
4900 print('ERROR: %s' % ex)
4901 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004902 return 0
4903
4904
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004905def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004906 """Prints info about try jobs associated with current CL."""
4907 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004908 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004909 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004910 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004911 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004912 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004913 '--color', action='store_true', default=setup_color.IS_TTY,
4914 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004915 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004916 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4917 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004918 group.add_option(
4919 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004920 parser.add_option_group(group)
4921 auth.add_auth_options(parser)
4922 options, args = parser.parse_args(args)
4923 if args:
4924 parser.error('Unrecognized args: %s' % ' '.join(args))
4925
4926 auth_config = auth.extract_auth_config_from_options(options)
4927 cl = Changelist(auth_config=auth_config)
4928 if not cl.GetIssue():
4929 parser.error('Need to upload first')
4930
tandrii221ab252016-10-06 08:12:04 -07004931 patchset = options.patchset
4932 if not patchset:
4933 patchset = cl.GetMostRecentPatchset()
4934 if not patchset:
4935 parser.error('Codereview doesn\'t know about issue %s. '
4936 'No access to issue or wrong issue number?\n'
4937 'Either upload first, or pass --patchset explicitely' %
4938 cl.GetIssue())
4939
4940 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004941 print('Warning: Codereview server has newer patchsets (%s) than most '
4942 'recent upload from local checkout (%s). Did a previous upload '
4943 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004944 'By default, git cl try-results uses the latest patchset from '
4945 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004946 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004947 try:
tandrii221ab252016-10-06 08:12:04 -07004948 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004949 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004950 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004951 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004952 if options.json:
4953 write_try_results_json(options.json, jobs)
4954 else:
4955 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004956 return 0
4957
4958
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004959@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004960def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004961 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004962 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004963 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004964 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004966 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004967 if args:
4968 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004969 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004970 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004971 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004972 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004973
4974 # Clear configured merge-base, if there is one.
4975 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004976 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004977 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004978 return 0
4979
4980
thestig@chromium.org00858c82013-12-02 23:08:03 +00004981def CMDweb(parser, args):
4982 """Opens the current CL in the web browser."""
4983 _, args = parser.parse_args(args)
4984 if args:
4985 parser.error('Unrecognized args: %s' % ' '.join(args))
4986
4987 issue_url = Changelist().GetIssueURL()
4988 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004990 return 1
4991
4992 webbrowser.open(issue_url)
4993 return 0
4994
4995
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004996def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004997 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004998 parser.add_option('-d', '--dry-run', action='store_true',
4999 help='trigger in dry run mode')
5000 parser.add_option('-c', '--clear', action='store_true',
5001 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005002 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005003 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005004 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005005 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005006 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005007 if args:
5008 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005009 if options.dry_run and options.clear:
5010 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5011
iannuccie53c9352016-08-17 14:40:40 -07005012 cl = Changelist(auth_config=auth_config, issue=options.issue,
5013 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005014 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005015 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005016 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005017 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005018 state = _CQState.DRY_RUN
5019 else:
5020 state = _CQState.COMMIT
5021 if not cl.GetIssue():
5022 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005023 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005024 return 0
5025
5026
groby@chromium.org411034a2013-02-26 15:12:01 +00005027def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005028 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005029 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005030 auth.add_auth_options(parser)
5031 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005032 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005033 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005034 if args:
5035 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005036 cl = Changelist(auth_config=auth_config, issue=options.issue,
5037 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005038 # Ensure there actually is an issue to close.
5039 cl.GetDescription()
5040 cl.CloseIssue()
5041 return 0
5042
5043
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005044def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005045 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005046 parser.add_option(
5047 '--stat',
5048 action='store_true',
5049 dest='stat',
5050 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005051 auth.add_auth_options(parser)
5052 options, args = parser.parse_args(args)
5053 auth_config = auth.extract_auth_config_from_options(options)
5054 if args:
5055 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005056
5057 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005058 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005059 # Staged changes would be committed along with the patch from last
5060 # upload, hence counted toward the "last upload" side in the final
5061 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005062 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005063 return 1
5064
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005065 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005066 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005067 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005068 if not issue:
5069 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005070 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005071 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005072
5073 # Create a new branch based on the merge-base
5074 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005075 # Clear cached branch in cl object, to avoid overwriting original CL branch
5076 # properties.
5077 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005078 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005079 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005080 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005081 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005082 return rtn
5083
wychen@chromium.org06928532015-02-03 02:11:29 +00005084 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005085 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005086 cmd = ['git', 'diff']
5087 if options.stat:
5088 cmd.append('--stat')
5089 cmd.extend([TMP_BRANCH, branch, '--'])
5090 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005091 finally:
5092 RunGit(['checkout', '-q', branch])
5093 RunGit(['branch', '-D', TMP_BRANCH])
5094
5095 return 0
5096
5097
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005098def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005099 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005100 parser.add_option(
5101 '--no-color',
5102 action='store_true',
5103 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005104 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005105 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005106 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005107
5108 author = RunGit(['config', 'user.email']).strip() or None
5109
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005110 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005111
5112 if args:
5113 if len(args) > 1:
5114 parser.error('Unknown args')
5115 base_branch = args[0]
5116 else:
5117 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005118 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005119
5120 change = cl.GetChange(base_branch, None)
5121 return owners_finder.OwnersFinder(
5122 [f.LocalPath() for f in
5123 cl.GetChange(base_branch, None).AffectedFiles()],
5124 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005125 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005126 disable_color=options.no_color).run()
5127
5128
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005129def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005130 """Generates a diff command."""
5131 # Generate diff for the current branch's changes.
5132 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5133 upstream_commit, '--' ]
5134
5135 if args:
5136 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005137 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005138 diff_cmd.append(arg)
5139 else:
5140 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005141
5142 return diff_cmd
5143
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005144def MatchingFileType(file_name, extensions):
5145 """Returns true if the file name ends with one of the given extensions."""
5146 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005147
enne@chromium.org555cfe42014-01-29 18:21:39 +00005148@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005149def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005150 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005151 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005152 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005153 parser.add_option('--full', action='store_true',
5154 help='Reformat the full content of all touched files')
5155 parser.add_option('--dry-run', action='store_true',
5156 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005157 parser.add_option('--python', action='store_true',
5158 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005159 parser.add_option('--diff', action='store_true',
5160 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005161 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005162
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005163 # git diff generates paths against the root of the repository. Change
5164 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005165 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005166 if rel_base_path:
5167 os.chdir(rel_base_path)
5168
digit@chromium.org29e47272013-05-17 17:01:46 +00005169 # Grab the merge-base commit, i.e. the upstream commit of the current
5170 # branch when it was created or the last time it was rebased. This is
5171 # to cover the case where the user may have called "git fetch origin",
5172 # moving the origin branch to a newer commit, but hasn't rebased yet.
5173 upstream_commit = None
5174 cl = Changelist()
5175 upstream_branch = cl.GetUpstreamBranch()
5176 if upstream_branch:
5177 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5178 upstream_commit = upstream_commit.strip()
5179
5180 if not upstream_commit:
5181 DieWithError('Could not find base commit for this branch. '
5182 'Are you in detached state?')
5183
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005184 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5185 diff_output = RunGit(changed_files_cmd)
5186 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005187 # Filter out files deleted by this CL
5188 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005189
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005190 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5191 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5192 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005193 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005194
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005195 top_dir = os.path.normpath(
5196 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5197
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005198 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5199 # formatted. This is used to block during the presubmit.
5200 return_value = 0
5201
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005202 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005203 # Locate the clang-format binary in the checkout
5204 try:
5205 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005206 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005207 DieWithError(e)
5208
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005209 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005210 cmd = [clang_format_tool]
5211 if not opts.dry_run and not opts.diff:
5212 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005213 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005214 if opts.diff:
5215 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005216 else:
5217 env = os.environ.copy()
5218 env['PATH'] = str(os.path.dirname(clang_format_tool))
5219 try:
5220 script = clang_format.FindClangFormatScriptInChromiumTree(
5221 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005222 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005223 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005224
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005225 cmd = [sys.executable, script, '-p0']
5226 if not opts.dry_run and not opts.diff:
5227 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005228
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005229 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5230 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005231
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005232 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5233 if opts.diff:
5234 sys.stdout.write(stdout)
5235 if opts.dry_run and len(stdout) > 0:
5236 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005237
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005238 # Similar code to above, but using yapf on .py files rather than clang-format
5239 # on C/C++ files
5240 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005241 yapf_tool = gclient_utils.FindExecutable('yapf')
5242 if yapf_tool is None:
5243 DieWithError('yapf not found in PATH')
5244
5245 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005246 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005247 cmd = [yapf_tool]
5248 if not opts.dry_run and not opts.diff:
5249 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005250 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005251 if opts.diff:
5252 sys.stdout.write(stdout)
5253 else:
5254 # TODO(sbc): yapf --lines mode still has some issues.
5255 # https://github.com/google/yapf/issues/154
5256 DieWithError('--python currently only works with --full')
5257
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005258 # Dart's formatter does not have the nice property of only operating on
5259 # modified chunks, so hard code full.
5260 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005261 try:
5262 command = [dart_format.FindDartFmtToolInChromiumTree()]
5263 if not opts.dry_run and not opts.diff:
5264 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005265 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005266
ppi@chromium.org6593d932016-03-03 15:41:15 +00005267 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005268 if opts.dry_run and stdout:
5269 return_value = 2
5270 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005271 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5272 'found in this checkout. Files in other languages are still '
5273 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005274
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005275 # Format GN build files. Always run on full build files for canonical form.
5276 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005277 cmd = ['gn', 'format' ]
5278 if opts.dry_run or opts.diff:
5279 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005280 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005281 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5282 shell=sys.platform == 'win32',
5283 cwd=top_dir)
5284 if opts.dry_run and gn_ret == 2:
5285 return_value = 2 # Not formatted.
5286 elif opts.diff and gn_ret == 2:
5287 # TODO this should compute and print the actual diff.
5288 print("This change has GN build file diff for " + gn_diff_file)
5289 elif gn_ret != 0:
5290 # For non-dry run cases (and non-2 return values for dry-run), a
5291 # nonzero error code indicates a failure, probably because the file
5292 # doesn't parse.
5293 DieWithError("gn format failed on " + gn_diff_file +
5294 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005295
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005296 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005297
5298
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005299@subcommand.usage('<codereview url or issue id>')
5300def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005301 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005302 _, args = parser.parse_args(args)
5303
5304 if len(args) != 1:
5305 parser.print_help()
5306 return 1
5307
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005308 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005309 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005310 parser.print_help()
5311 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005312 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005313
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005314 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005315 output = RunGit(['config', '--local', '--get-regexp',
5316 r'branch\..*\.%s' % issueprefix],
5317 error_ok=True)
5318 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005319 if issue == target_issue:
5320 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005321
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005322 branches = []
5323 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005324 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005325 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005326 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327 return 1
5328 if len(branches) == 1:
5329 RunGit(['checkout', branches[0]])
5330 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005331 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005332 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005333 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005334 which = raw_input('Choose by index: ')
5335 try:
5336 RunGit(['checkout', branches[int(which)]])
5337 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005338 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005339 return 1
5340
5341 return 0
5342
5343
maruel@chromium.org29404b52014-09-08 22:58:00 +00005344def CMDlol(parser, args):
5345 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005346 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005347 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5348 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5349 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005350 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005351 return 0
5352
5353
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005354class OptionParser(optparse.OptionParser):
5355 """Creates the option parse and add --verbose support."""
5356 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005357 optparse.OptionParser.__init__(
5358 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005359 self.add_option(
5360 '-v', '--verbose', action='count', default=0,
5361 help='Use 2 times for more debugging info')
5362
5363 def parse_args(self, args=None, values=None):
5364 options, args = optparse.OptionParser.parse_args(self, args, values)
5365 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5366 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5367 return options, args
5368
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005370def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005371 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005372 print('\nYour python version %s is unsupported, please upgrade.\n' %
5373 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005374 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005375
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005376 # Reload settings.
5377 global settings
5378 settings = Settings()
5379
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005380 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005381 dispatcher = subcommand.CommandDispatcher(__name__)
5382 try:
5383 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005384 except auth.AuthenticationError as e:
5385 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005386 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005387 if e.code != 500:
5388 raise
5389 DieWithError(
5390 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5391 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005392 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005393
5394
5395if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005396 # These affect sys.stdout so do it outside of main() to simplify mocks in
5397 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005398 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005399 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005400 try:
5401 sys.exit(main(sys.argv[1:]))
5402 except KeyboardInterrupt:
5403 sys.stderr.write('interrupted\n')
5404 sys.exit(1)