blob: 66bb76831aa3c1608b5b06b7585353be41579cb4 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
skobes6468b902016-10-24 08:45:10 -070043import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
88
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070090 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091 sys.exit(1)
92
93
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000094def GetNoGitPagerEnv():
95 env = os.environ.copy()
96 # 'cat' is a magical git string that disables pagers on all platforms.
97 env['GIT_PAGER'] = 'cat'
98 return env
99
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000100
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000104 except subprocess2.CalledProcessError as e:
105 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 'Command "%s" failed.\n%s' % (
109 ' '.join(args), error_message or e.stdout or ''))
110 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
113def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000115 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000119 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 try:
tandrii5d48c322016-08-18 16:19:37 -0700125 (out, _), code = subprocess2.communicate(['git'] + args,
126 env=GetNoGitPagerEnv(),
127 stdout=subprocess2.PIPE,
128 stderr=stderr)
129 return code, out
130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
132 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
tandrii2a16b952016-10-19 07:09:44 -0700154def time_sleep(seconds):
155 # Use this so that it can be mocked in tests without interfering with python
156 # system machinery.
157 import time # Local import to discourage others from importing time globally.
158 return time.sleep(seconds)
159
160
maruel@chromium.org90541732011-04-01 17:54:18 +0000161def ask_for_data(prompt):
162 try:
163 return raw_input(prompt)
164 except KeyboardInterrupt:
165 # Hide the exception.
166 sys.exit(1)
167
168
tandrii5d48c322016-08-18 16:19:37 -0700169def _git_branch_config_key(branch, key):
170 """Helper method to return Git config key for a branch."""
171 assert branch, 'branch name is required to set git config for it'
172 return 'branch.%s.%s' % (branch, key)
173
174
175def _git_get_branch_config_value(key, default=None, value_type=str,
176 branch=False):
177 """Returns git config value of given or current branch if any.
178
179 Returns default in all other cases.
180 """
181 assert value_type in (int, str, bool)
182 if branch is False: # Distinguishing default arg value from None.
183 branch = GetCurrentBranch()
184
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000185 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700186 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187
tandrii5d48c322016-08-18 16:19:37 -0700188 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700189 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700190 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700191 # git config also has --int, but apparently git config suffers from integer
192 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700193 args.append(_git_branch_config_key(branch, key))
194 code, out = RunGitWithCode(args)
195 if code == 0:
196 value = out.strip()
197 if value_type == int:
198 return int(value)
199 if value_type == bool:
200 return bool(value.lower() == 'true')
201 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 return default
203
204
tandrii5d48c322016-08-18 16:19:37 -0700205def _git_set_branch_config_value(key, value, branch=None, **kwargs):
206 """Sets the value or unsets if it's None of a git branch config.
207
208 Valid, though not necessarily existing, branch must be provided,
209 otherwise currently checked out branch is used.
210 """
211 if not branch:
212 branch = GetCurrentBranch()
213 assert branch, 'a branch name OR currently checked out branch is required'
214 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700215 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700216 if value is None:
217 args.append('--unset')
218 elif isinstance(value, bool):
219 args.append('--bool')
220 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700221 else:
tandrii33a46ff2016-08-23 05:53:40 -0700222 # git config also has --int, but apparently git config suffers from integer
223 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700224 value = str(value)
225 args.append(_git_branch_config_key(branch, key))
226 if value is not None:
227 args.append(value)
228 RunGit(args, **kwargs)
229
230
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000231def add_git_similarity(parser):
232 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700233 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000234 help='Sets the percentage that a pair of files need to match in order to'
235 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000236 parser.add_option(
237 '--find-copies', action='store_true',
238 help='Allows git to look for copies.')
239 parser.add_option(
240 '--no-find-copies', action='store_false', dest='find_copies',
241 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000242
243 old_parser_args = parser.parse_args
244 def Parse(args):
245 options, args = old_parser_args(args)
246
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000247 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.similarity = _git_get_branch_config_value(
249 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 print('Note: Saving similarity of %d%% in git config.'
252 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700253 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 options.similarity = max(0, min(options.similarity, 100))
256
257 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700258 options.find_copies = _git_get_branch_config_value(
259 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000260 else:
tandrii5d48c322016-08-18 16:19:37 -0700261 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 print('Using %d%% similarity for rename/copy detection. '
264 'Override with --similarity.' % options.similarity)
265
266 return options, args
267 parser.parse_args = Parse
268
269
machenbach@chromium.org45453142015-09-15 08:45:22 +0000270def _get_properties_from_options(options):
271 properties = dict(x.split('=', 1) for x in options.properties)
272 for key, val in properties.iteritems():
273 try:
274 properties[key] = json.loads(val)
275 except ValueError:
276 pass # If a value couldn't be evaluated, treat it as a string.
277 return properties
278
279
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280def _prefix_master(master):
281 """Convert user-specified master name to full master name.
282
283 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
284 name, while the developers always use shortened master name
285 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
286 function does the conversion for buildbucket migration.
287 """
borenet6c0efe62016-10-19 08:13:29 -0700288 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 return master
borenet6c0efe62016-10-19 08:13:29 -0700290 return '%s%s' % (MASTER_PREFIX, master)
291
292
293def _unprefix_master(bucket):
294 """Convert bucket name to shortened master name.
295
296 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
297 name, while the developers always use shortened master name
298 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
299 function does the conversion for buildbucket migration.
300 """
301 if bucket.startswith(MASTER_PREFIX):
302 return bucket[len(MASTER_PREFIX):]
303 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306def _buildbucket_retry(operation_name, http, *args, **kwargs):
307 """Retries requests to buildbucket service and returns parsed json content."""
308 try_count = 0
309 while True:
310 response, content = http.request(*args, **kwargs)
311 try:
312 content_json = json.loads(content)
313 except ValueError:
314 content_json = None
315
316 # Buildbucket could return an error even if status==200.
317 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000318 error = content_json.get('error')
319 if error.get('code') == 403:
320 raise BuildbucketResponseException(
321 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000322 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000323 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000324 raise BuildbucketResponseException(msg)
325
326 if response.status == 200:
327 if not content_json:
328 raise BuildbucketResponseException(
329 'Buildbucket returns invalid json content: %s.\n'
330 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
331 content)
332 return content_json
333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content)
335
336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700338 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 try_count += 1
340 assert False, 'unreachable'
341
342
qyearsley1fdfcb62016-10-24 13:22:03 -0700343def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700344 """Returns a dict mapping bucket names to builders and tests,
345 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700346 """
qyearsleydd49f942016-10-28 11:57:22 -0700347 # If no bots are listed, we try to get a set of builders and tests based
348 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700349 if not options.bot:
350 change = changelist.GetChange(
351 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700352 # Get try masters from PRESUBMIT.py files.
353 return presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700354 change=change,
355 changed_files=change.LocalPaths(),
356 repository_root=settings.GetRoot(),
357 default_presubmit=None,
358 project=None,
359 verbose=options.verbose,
360 output_stream=sys.stdout)
361
qyearsley1fdfcb62016-10-24 13:22:03 -0700362 if options.bucket:
363 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700364 if options.master:
365 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700366
qyearsleydd49f942016-10-28 11:57:22 -0700367 # If bots are listed but no master or bucket, then we need to find out
368 # the corresponding master for each bot.
369 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
370 if error_message:
371 option_parser.error(
372 'Tryserver master cannot be found because: %s\n'
373 'Please manually specify the tryserver master, e.g. '
374 '"-m tryserver.chromium.linux".' % error_message)
375 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700376
377
qyearsley123a4682016-10-26 09:12:17 -0700378def _get_bucket_map_for_builders(builders):
379 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700380 map_url = 'https://builders-map.appspot.com/'
381 try:
qyearsley123a4682016-10-26 09:12:17 -0700382 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 except urllib2.URLError as e:
384 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
385 (map_url, e))
386 except ValueError as e:
387 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700388 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 return None, 'Failed to build master map.'
390
qyearsley123a4682016-10-26 09:12:17 -0700391 bucket_map = {}
392 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700393 masters = builders_map.get(builder, [])
394 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700395 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700396 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700397 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700398 (builder, masters))
399 bucket = _prefix_master(masters[0])
400 bucket_map.setdefault(bucket, {})[builder] = []
401
402 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700403
404
borenet6c0efe62016-10-19 08:13:29 -0700405def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700406 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 """Sends a request to Buildbucket to trigger try jobs for a changelist.
408
409 Args:
410 auth_config: AuthConfig for Rietveld.
411 changelist: Changelist that the try jobs are associated with.
412 buckets: A nested dict mapping bucket names to builders to tests.
413 options: Command-line options.
414 """
tandriide281ae2016-10-12 06:02:30 -0700415 assert changelist.GetIssue(), 'CL must be uploaded first'
416 codereview_url = changelist.GetCodereviewServer()
417 assert codereview_url, 'CL must be uploaded first'
418 patchset = patchset or changelist.GetMostRecentPatchset()
419 assert patchset, 'CL must be uploaded first'
420
421 codereview_host = urlparse.urlparse(codereview_url).hostname
422 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000423 http = authenticator.authorize(httplib2.Http())
424 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700425
426 # TODO(tandrii): consider caching Gerrit CL details just like
427 # _RietveldChangelistImpl does, then caching values in these two variables
428 # won't be necessary.
429 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000430
431 buildbucket_put_url = (
432 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000433 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700434 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
435 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
436 hostname=codereview_host,
437 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000438 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700439
440 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
441 shared_parameters_properties['category'] = category
442 if options.clobber:
443 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700444 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700445 if extra_properties:
446 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000447
448 batch_req_body = {'builds': []}
449 print_text = []
450 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700451 for bucket, builders_and_tests in sorted(buckets.iteritems()):
452 print_text.append('Bucket: %s' % bucket)
453 master = None
454 if bucket.startswith(MASTER_PREFIX):
455 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 for builder, tests in sorted(builders_and_tests.iteritems()):
457 print_text.append(' %s: %s' % (builder, tests))
458 parameters = {
459 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000460 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700461 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000462 'revision': options.revision,
463 }],
tandrii8c5a3532016-11-04 07:52:02 -0700464 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000466 if 'presubmit' in builder.lower():
467 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000468 if tests:
469 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700470
471 tags = [
472 'builder:%s' % builder,
473 'buildset:%s' % buildset,
474 'user_agent:git_cl_try',
475 ]
476 if master:
477 parameters['properties']['master'] = master
478 tags.append('master:%s' % master)
479
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000480 batch_req_body['builds'].append(
481 {
482 'bucket': bucket,
483 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000484 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700485 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 }
487 )
488
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000489 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700490 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000491 http,
492 buildbucket_put_url,
493 'PUT',
494 body=json.dumps(batch_req_body),
495 headers={'Content-Type': 'application/json'}
496 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000497 print_text.append('To see results here, run: git cl try-results')
498 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700499 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000500
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000501
tandrii221ab252016-10-06 08:12:04 -0700502def fetch_try_jobs(auth_config, changelist, buildbucket_host,
503 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700504 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505
qyearsley53f48a12016-09-01 10:45:13 -0700506 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507 """
tandrii221ab252016-10-06 08:12:04 -0700508 assert buildbucket_host
509 assert changelist.GetIssue(), 'CL must be uploaded first'
510 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
511 patchset = patchset or changelist.GetMostRecentPatchset()
512 assert patchset, 'CL must be uploaded first'
513
514 codereview_url = changelist.GetCodereviewServer()
515 codereview_host = urlparse.urlparse(codereview_url).hostname
516 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 if authenticator.has_cached_credentials():
518 http = authenticator.authorize(httplib2.Http())
519 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700520 print('Warning: Some results might be missing because %s' %
521 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700522 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000523 http = httplib2.Http()
524
525 http.force_exception_to_status_code = True
526
tandrii221ab252016-10-06 08:12:04 -0700527 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
528 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
529 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000530 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700531 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532 params = {'tag': 'buildset:%s' % buildset}
533
534 builds = {}
535 while True:
536 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700537 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700539 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 for build in content.get('builds', []):
541 builds[build['id']] = build
542 if 'next_cursor' in content:
543 params['start_cursor'] = content['next_cursor']
544 else:
545 break
546 return builds
547
548
qyearsleyeab3c042016-08-24 09:18:28 -0700549def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 """Prints nicely result of fetch_try_jobs."""
551 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700552 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 return
554
555 # Make a copy, because we'll be modifying builds dictionary.
556 builds = builds.copy()
557 builder_names_cache = {}
558
559 def get_builder(b):
560 try:
561 return builder_names_cache[b['id']]
562 except KeyError:
563 try:
564 parameters = json.loads(b['parameters_json'])
565 name = parameters['builder_name']
566 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700567 print('WARNING: failed to get builder name for build %s: %s' % (
568 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 name = None
570 builder_names_cache[b['id']] = name
571 return name
572
573 def get_bucket(b):
574 bucket = b['bucket']
575 if bucket.startswith('master.'):
576 return bucket[len('master.'):]
577 return bucket
578
579 if options.print_master:
580 name_fmt = '%%-%ds %%-%ds' % (
581 max(len(str(get_bucket(b))) for b in builds.itervalues()),
582 max(len(str(get_builder(b))) for b in builds.itervalues()))
583 def get_name(b):
584 return name_fmt % (get_bucket(b), get_builder(b))
585 else:
586 name_fmt = '%%-%ds' % (
587 max(len(str(get_builder(b))) for b in builds.itervalues()))
588 def get_name(b):
589 return name_fmt % get_builder(b)
590
591 def sort_key(b):
592 return b['status'], b.get('result'), get_name(b), b.get('url')
593
594 def pop(title, f, color=None, **kwargs):
595 """Pop matching builds from `builds` dict and print them."""
596
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000597 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 colorize = str
599 else:
600 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
601
602 result = []
603 for b in builds.values():
604 if all(b.get(k) == v for k, v in kwargs.iteritems()):
605 builds.pop(b['id'])
606 result.append(b)
607 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700608 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000609 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700610 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611
612 total = len(builds)
613 pop(status='COMPLETED', result='SUCCESS',
614 title='Successes:', color=Fore.GREEN,
615 f=lambda b: (get_name(b), b.get('url')))
616 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
617 title='Infra Failures:', color=Fore.MAGENTA,
618 f=lambda b: (get_name(b), b.get('url')))
619 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
620 title='Failures:', color=Fore.RED,
621 f=lambda b: (get_name(b), b.get('url')))
622 pop(status='COMPLETED', result='CANCELED',
623 title='Canceled:', color=Fore.MAGENTA,
624 f=lambda b: (get_name(b),))
625 pop(status='COMPLETED', result='FAILURE',
626 failure_reason='INVALID_BUILD_DEFINITION',
627 title='Wrong master/builder name:', color=Fore.MAGENTA,
628 f=lambda b: (get_name(b),))
629 pop(status='COMPLETED', result='FAILURE',
630 title='Other failures:',
631 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
632 pop(status='COMPLETED',
633 title='Other finished:',
634 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
635 pop(status='STARTED',
636 title='Started:', color=Fore.YELLOW,
637 f=lambda b: (get_name(b), b.get('url')))
638 pop(status='SCHEDULED',
639 title='Scheduled:',
640 f=lambda b: (get_name(b), 'id=%s' % b['id']))
641 # The last section is just in case buildbucket API changes OR there is a bug.
642 pop(title='Other:',
643 f=lambda b: (get_name(b), 'id=%s' % b['id']))
644 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700645 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000646
647
qyearsley53f48a12016-09-01 10:45:13 -0700648def write_try_results_json(output_file, builds):
649 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
650
651 The input |builds| dict is assumed to be generated by Buildbucket.
652 Buildbucket documentation: http://goo.gl/G0s101
653 """
654
655 def convert_build_dict(build):
656 return {
657 'buildbucket_id': build.get('id'),
658 'status': build.get('status'),
659 'result': build.get('result'),
660 'bucket': build.get('bucket'),
661 'builder_name': json.loads(
662 build.get('parameters_json', '{}')).get('builder_name'),
663 'failure_reason': build.get('failure_reason'),
664 'url': build.get('url'),
665 }
666
667 converted = []
668 for _, build in sorted(builds.items()):
669 converted.append(convert_build_dict(build))
670 write_json(output_file, converted)
671
672
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000673def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
674 """Return the corresponding git ref if |base_url| together with |glob_spec|
675 matches the full |url|.
676
677 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
678 """
679 fetch_suburl, as_ref = glob_spec.split(':')
680 if allow_wildcards:
681 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
682 if glob_match:
683 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
684 # "branches/{472,597,648}/src:refs/remotes/svn/*".
685 branch_re = re.escape(base_url)
686 if glob_match.group(1):
687 branch_re += '/' + re.escape(glob_match.group(1))
688 wildcard = glob_match.group(2)
689 if wildcard == '*':
690 branch_re += '([^/]*)'
691 else:
692 # Escape and replace surrounding braces with parentheses and commas
693 # with pipe symbols.
694 wildcard = re.escape(wildcard)
695 wildcard = re.sub('^\\\\{', '(', wildcard)
696 wildcard = re.sub('\\\\,', '|', wildcard)
697 wildcard = re.sub('\\\\}$', ')', wildcard)
698 branch_re += wildcard
699 if glob_match.group(3):
700 branch_re += re.escape(glob_match.group(3))
701 match = re.match(branch_re, url)
702 if match:
703 return re.sub('\*$', match.group(1), as_ref)
704
705 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
706 if fetch_suburl:
707 full_url = base_url + '/' + fetch_suburl
708 else:
709 full_url = base_url
710 if full_url == url:
711 return as_ref
712 return None
713
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000714
iannucci@chromium.org79540052012-10-19 23:15:26 +0000715def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000716 """Prints statistics about the change to the user."""
717 # --no-ext-diff is broken in some versions of Git, so try to work around
718 # this by overriding the environment (but there is still a problem if the
719 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000720 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000721 if 'GIT_EXTERNAL_DIFF' in env:
722 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000723
724 if find_copies:
725 similarity_options = ['--find-copies-harder', '-l100000',
726 '-C%s' % similarity]
727 else:
728 similarity_options = ['-M%s' % similarity]
729
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000730 try:
731 stdout = sys.stdout.fileno()
732 except AttributeError:
733 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000734 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000735 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000736 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000737 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000738
739
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000740class BuildbucketResponseException(Exception):
741 pass
742
743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744class Settings(object):
745 def __init__(self):
746 self.default_server = None
747 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000748 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749 self.is_git_svn = None
750 self.svn_branch = None
751 self.tree_status_url = None
752 self.viewvc_url = None
753 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000754 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000755 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000756 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000757 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000758 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000759 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000760 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700761 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762
763 def LazyUpdateIfNeeded(self):
764 """Updates the settings from a codereview.settings file, if available."""
765 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000766 # The only value that actually changes the behavior is
767 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000768 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000769 error_ok=True
770 ).strip().lower()
771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000773 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774 LoadCodereviewSettingsFromFile(cr_settings_file)
775 self.updated = True
776
777 def GetDefaultServerUrl(self, error_ok=False):
778 if not self.default_server:
779 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000780 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000781 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 if error_ok:
783 return self.default_server
784 if not self.default_server:
785 error_message = ('Could not find settings file. You must configure '
786 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000787 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 return self.default_server
790
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000791 @staticmethod
792 def GetRelativeRoot():
793 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000796 if self.root is None:
797 self.root = os.path.abspath(self.GetRelativeRoot())
798 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000800 def GetGitMirror(self, remote='origin'):
801 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000802 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000803 if not os.path.isdir(local_url):
804 return None
805 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
806 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
807 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
808 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
809 if mirror.exists():
810 return mirror
811 return None
812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813 def GetIsGitSvn(self):
814 """Return true if this repo looks like it's using git-svn."""
815 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000816 if self.GetPendingRefPrefix():
817 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
818 self.is_git_svn = False
819 else:
820 # If you have any "svn-remote.*" config keys, we think you're using svn.
821 self.is_git_svn = RunGitWithCode(
822 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823 return self.is_git_svn
824
825 def GetSVNBranch(self):
826 if self.svn_branch is None:
827 if not self.GetIsGitSvn():
828 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
829
830 # Try to figure out which remote branch we're based on.
831 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000832 # 1) iterate through our branch history and find the svn URL.
833 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834
835 # regexp matching the git-svn line that contains the URL.
836 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
837
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000838 # We don't want to go through all of history, so read a line from the
839 # pipe at a time.
840 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000841 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
843 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000844 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000845 for line in proc.stdout:
846 match = git_svn_re.match(line)
847 if match:
848 url = match.group(1)
849 proc.stdout.close() # Cut pipe.
850 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000852 if url:
853 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
854 remotes = RunGit(['config', '--get-regexp',
855 r'^svn-remote\..*\.url']).splitlines()
856 for remote in remotes:
857 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000859 remote = match.group(1)
860 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000861 rewrite_root = RunGit(
862 ['config', 'svn-remote.%s.rewriteRoot' % remote],
863 error_ok=True).strip()
864 if rewrite_root:
865 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000866 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000867 ['config', 'svn-remote.%s.fetch' % remote],
868 error_ok=True).strip()
869 if fetch_spec:
870 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
871 if self.svn_branch:
872 break
873 branch_spec = RunGit(
874 ['config', 'svn-remote.%s.branches' % remote],
875 error_ok=True).strip()
876 if branch_spec:
877 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
878 if self.svn_branch:
879 break
880 tag_spec = RunGit(
881 ['config', 'svn-remote.%s.tags' % remote],
882 error_ok=True).strip()
883 if tag_spec:
884 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
885 if self.svn_branch:
886 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887
888 if not self.svn_branch:
889 DieWithError('Can\'t guess svn branch -- try specifying it on the '
890 'command line')
891
892 return self.svn_branch
893
894 def GetTreeStatusUrl(self, error_ok=False):
895 if not self.tree_status_url:
896 error_message = ('You must configure your tree status URL by running '
897 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000898 self.tree_status_url = self._GetRietveldConfig(
899 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 return self.tree_status_url
901
902 def GetViewVCUrl(self):
903 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000904 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 return self.viewvc_url
906
rmistry@google.com90752582014-01-14 21:04:50 +0000907 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000908 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000909
rmistry@google.com78948ed2015-07-08 23:09:57 +0000910 def GetIsSkipDependencyUpload(self, branch_name):
911 """Returns true if specified branch should skip dep uploads."""
912 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
913 error_ok=True)
914
rmistry@google.com5626a922015-02-26 14:03:30 +0000915 def GetRunPostUploadHook(self):
916 run_post_upload_hook = self._GetRietveldConfig(
917 'run-post-upload-hook', error_ok=True)
918 return run_post_upload_hook == "True"
919
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000920 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000921 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000922
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000923 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000924 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000925
ukai@chromium.orge8077812012-02-03 03:41:46 +0000926 def GetIsGerrit(self):
927 """Return true if this repo is assosiated with gerrit code review system."""
928 if self.is_gerrit is None:
929 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
930 return self.is_gerrit
931
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000932 def GetSquashGerritUploads(self):
933 """Return true if uploads to Gerrit should be squashed by default."""
934 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700935 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
936 if self.squash_gerrit_uploads is None:
937 # Default is squash now (http://crbug.com/611892#c23).
938 self.squash_gerrit_uploads = not (
939 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
940 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000941 return self.squash_gerrit_uploads
942
tandriia60502f2016-06-20 02:01:53 -0700943 def GetSquashGerritUploadsOverride(self):
944 """Return True or False if codereview.settings should be overridden.
945
946 Returns None if no override has been defined.
947 """
948 # See also http://crbug.com/611892#c23
949 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
950 error_ok=True).strip()
951 if result == 'true':
952 return True
953 if result == 'false':
954 return False
955 return None
956
tandrii@chromium.org28253532016-04-14 13:46:56 +0000957 def GetGerritSkipEnsureAuthenticated(self):
958 """Return True if EnsureAuthenticated should not be done for Gerrit
959 uploads."""
960 if self.gerrit_skip_ensure_authenticated is None:
961 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000962 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000963 error_ok=True).strip() == 'true')
964 return self.gerrit_skip_ensure_authenticated
965
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000966 def GetGitEditor(self):
967 """Return the editor specified in the git config, or None if none is."""
968 if self.git_editor is None:
969 self.git_editor = self._GetConfig('core.editor', error_ok=True)
970 return self.git_editor or None
971
thestig@chromium.org44202a22014-03-11 19:22:18 +0000972 def GetLintRegex(self):
973 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
974 DEFAULT_LINT_REGEX)
975
976 def GetLintIgnoreRegex(self):
977 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
978 DEFAULT_LINT_IGNORE_REGEX)
979
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000980 def GetProject(self):
981 if not self.project:
982 self.project = self._GetRietveldConfig('project', error_ok=True)
983 return self.project
984
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000985 def GetForceHttpsCommitUrl(self):
986 if not self.force_https_commit_url:
987 self.force_https_commit_url = self._GetRietveldConfig(
988 'force-https-commit-url', error_ok=True)
989 return self.force_https_commit_url
990
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000991 def GetPendingRefPrefix(self):
992 if not self.pending_ref_prefix:
993 self.pending_ref_prefix = self._GetRietveldConfig(
994 'pending-ref-prefix', error_ok=True)
995 return self.pending_ref_prefix
996
tandriif46c20f2016-09-14 06:17:05 -0700997 def GetHasGitNumberFooter(self):
998 # TODO(tandrii): this has to be removed after Rietveld is read-only.
999 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1000 if not self.git_number_footer:
1001 self.git_number_footer = self._GetRietveldConfig(
1002 'git-number-footer', error_ok=True)
1003 return self.git_number_footer
1004
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001005 def _GetRietveldConfig(self, param, **kwargs):
1006 return self._GetConfig('rietveld.' + param, **kwargs)
1007
rmistry@google.com78948ed2015-07-08 23:09:57 +00001008 def _GetBranchConfig(self, branch_name, param, **kwargs):
1009 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1010
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 def _GetConfig(self, param, **kwargs):
1012 self.LazyUpdateIfNeeded()
1013 return RunGit(['config', param], **kwargs).strip()
1014
1015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016def ShortBranchName(branch):
1017 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001018 return branch.replace('refs/heads/', '', 1)
1019
1020
1021def GetCurrentBranchRef():
1022 """Returns branch ref (e.g., refs/heads/master) or None."""
1023 return RunGit(['symbolic-ref', 'HEAD'],
1024 stderr=subprocess2.VOID, error_ok=True).strip() or None
1025
1026
1027def GetCurrentBranch():
1028 """Returns current branch or None.
1029
1030 For refs/heads/* branches, returns just last part. For others, full ref.
1031 """
1032 branchref = GetCurrentBranchRef()
1033 if branchref:
1034 return ShortBranchName(branchref)
1035 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036
1037
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001038class _CQState(object):
1039 """Enum for states of CL with respect to Commit Queue."""
1040 NONE = 'none'
1041 DRY_RUN = 'dry_run'
1042 COMMIT = 'commit'
1043
1044 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1045
1046
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001047class _ParsedIssueNumberArgument(object):
1048 def __init__(self, issue=None, patchset=None, hostname=None):
1049 self.issue = issue
1050 self.patchset = patchset
1051 self.hostname = hostname
1052
1053 @property
1054 def valid(self):
1055 return self.issue is not None
1056
1057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001058def ParseIssueNumberArgument(arg):
1059 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1060 fail_result = _ParsedIssueNumberArgument()
1061
1062 if arg.isdigit():
1063 return _ParsedIssueNumberArgument(issue=int(arg))
1064 if not arg.startswith('http'):
1065 return fail_result
1066 url = gclient_utils.UpgradeToHttps(arg)
1067 try:
1068 parsed_url = urlparse.urlparse(url)
1069 except ValueError:
1070 return fail_result
1071 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1072 tmp = cls.ParseIssueURL(parsed_url)
1073 if tmp is not None:
1074 return tmp
1075 return fail_result
1076
1077
tandriic2405f52016-10-10 08:13:15 -07001078class GerritIssueNotExists(Exception):
1079 def __init__(self, issue, url):
1080 self.issue = issue
1081 self.url = url
1082 super(GerritIssueNotExists, self).__init__()
1083
1084 def __str__(self):
1085 return 'issue %s at %s does not exist or you have no access to it' % (
1086 self.issue, self.url)
1087
1088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001090 """Changelist works with one changelist in local branch.
1091
1092 Supports two codereview backends: Rietveld or Gerrit, selected at object
1093 creation.
1094
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001095 Notes:
1096 * Not safe for concurrent multi-{thread,process} use.
1097 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001098 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001099 """
1100
1101 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1102 """Create a new ChangeList instance.
1103
1104 If issue is given, the codereview must be given too.
1105
1106 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1107 Otherwise, it's decided based on current configuration of the local branch,
1108 with default being 'rietveld' for backwards compatibility.
1109 See _load_codereview_impl for more details.
1110
1111 **kwargs will be passed directly to codereview implementation.
1112 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001114 global settings
1115 if not settings:
1116 # Happens when git_cl.py is used as a utility library.
1117 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001118
1119 if issue:
1120 assert codereview, 'codereview must be known, if issue is known'
1121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 self.branchref = branchref
1123 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001124 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 self.branch = ShortBranchName(self.branchref)
1126 else:
1127 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001129 self.lookedup_issue = False
1130 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 self.has_description = False
1132 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001133 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001135 self.cc = None
1136 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001137 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001138
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001140 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001142 assert self._codereview_impl
1143 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144
1145 def _load_codereview_impl(self, codereview=None, **kwargs):
1146 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001147 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1148 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1149 self._codereview = codereview
1150 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001151 return
1152
1153 # Automatic selection based on issue number set for a current branch.
1154 # Rietveld takes precedence over Gerrit.
1155 assert not self.issue
1156 # Whether we find issue or not, we are doing the lookup.
1157 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001158 if self.GetBranch():
1159 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1160 issue = _git_get_branch_config_value(
1161 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1162 if issue:
1163 self._codereview = codereview
1164 self._codereview_impl = cls(self, **kwargs)
1165 self.issue = int(issue)
1166 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167
1168 # No issue is set for this branch, so decide based on repo-wide settings.
1169 return self._load_codereview_impl(
1170 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1171 **kwargs)
1172
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001173 def IsGerrit(self):
1174 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001175
1176 def GetCCList(self):
1177 """Return the users cc'd on this CL.
1178
agable92bec4f2016-08-24 09:27:27 -07001179 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001180 """
1181 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001182 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001183 more_cc = ','.join(self.watchers)
1184 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1185 return self.cc
1186
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001187 def GetCCListWithoutDefault(self):
1188 """Return the users cc'd on this CL excluding default ones."""
1189 if self.cc is None:
1190 self.cc = ','.join(self.watchers)
1191 return self.cc
1192
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001193 def SetWatchers(self, watchers):
1194 """Set the list of email addresses that should be cc'd based on the changed
1195 files in this CL.
1196 """
1197 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
1199 def GetBranch(self):
1200 """Returns the short branch name, e.g. 'master'."""
1201 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001203 if not branchref:
1204 return None
1205 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 self.branch = ShortBranchName(self.branchref)
1207 return self.branch
1208
1209 def GetBranchRef(self):
1210 """Returns the full branch name, e.g. 'refs/heads/master'."""
1211 self.GetBranch() # Poke the lazy loader.
1212 return self.branchref
1213
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001214 def ClearBranch(self):
1215 """Clears cached branch data of this object."""
1216 self.branch = self.branchref = None
1217
tandrii5d48c322016-08-18 16:19:37 -07001218 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1219 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1220 kwargs['branch'] = self.GetBranch()
1221 return _git_get_branch_config_value(key, default, **kwargs)
1222
1223 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1224 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1225 assert self.GetBranch(), (
1226 'this CL must have an associated branch to %sset %s%s' %
1227 ('un' if value is None else '',
1228 key,
1229 '' if value is None else ' to %r' % value))
1230 kwargs['branch'] = self.GetBranch()
1231 return _git_set_branch_config_value(key, value, **kwargs)
1232
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001233 @staticmethod
1234 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001235 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 e.g. 'origin', 'refs/heads/master'
1237 """
1238 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001239 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001242 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001244 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1245 error_ok=True).strip()
1246 if upstream_branch:
1247 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001249 # Fall back on trying a git-svn upstream branch.
1250 if settings.GetIsGitSvn():
1251 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001253 # Else, try to guess the origin remote.
1254 remote_branches = RunGit(['branch', '-r']).split()
1255 if 'origin/master' in remote_branches:
1256 # Fall back on origin/master if it exits.
1257 remote = 'origin'
1258 upstream_branch = 'refs/heads/master'
1259 elif 'origin/trunk' in remote_branches:
1260 # Fall back on origin/trunk if it exists. Generally a shared
1261 # git-svn clone
1262 remote = 'origin'
1263 upstream_branch = 'refs/heads/trunk'
1264 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001265 DieWithError(
1266 'Unable to determine default branch to diff against.\n'
1267 'Either pass complete "git diff"-style arguments, like\n'
1268 ' git cl upload origin/master\n'
1269 'or verify this branch is set up to track another \n'
1270 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271
1272 return remote, upstream_branch
1273
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001274 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001275 upstream_branch = self.GetUpstreamBranch()
1276 if not BranchExists(upstream_branch):
1277 DieWithError('The upstream for the current branch (%s) does not exist '
1278 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001279 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001280 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001281
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 def GetUpstreamBranch(self):
1283 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001286 upstream_branch = upstream_branch.replace('refs/heads/',
1287 'refs/remotes/%s/' % remote)
1288 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1289 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 self.upstream_branch = upstream_branch
1291 return self.upstream_branch
1292
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001293 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001294 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001295 remote, branch = None, self.GetBranch()
1296 seen_branches = set()
1297 while branch not in seen_branches:
1298 seen_branches.add(branch)
1299 remote, branch = self.FetchUpstreamTuple(branch)
1300 branch = ShortBranchName(branch)
1301 if remote != '.' or branch.startswith('refs/remotes'):
1302 break
1303 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001304 remotes = RunGit(['remote'], error_ok=True).split()
1305 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001306 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001307 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309 logging.warning('Could not determine which remote this change is '
1310 'associated with, so defaulting to "%s". This may '
1311 'not be what you want. You may prevent this message '
1312 'by running "git svn info" as documented here: %s',
1313 self._remote,
1314 GIT_INSTRUCTIONS_URL)
1315 else:
1316 logging.warn('Could not determine which remote this change is '
1317 'associated with. You may prevent this message by '
1318 'running "git svn info" as documented here: %s',
1319 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 branch = 'HEAD'
1321 if branch.startswith('refs/remotes'):
1322 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001323 elif branch.startswith('refs/branch-heads/'):
1324 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001325 else:
1326 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327 return self._remote
1328
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 def GitSanityChecks(self, upstream_git_obj):
1330 """Checks git repo status and ensures diff is from local commits."""
1331
sbc@chromium.org79706062015-01-14 21:18:12 +00001332 if upstream_git_obj is None:
1333 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001334 print('ERROR: unable to determine current branch (detached HEAD?)',
1335 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001336 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001337 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 return False
1339
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 # Verify the commit we're diffing against is in our current branch.
1341 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1342 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1343 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001344 print('ERROR: %s is not in the current branch. You may need to rebase '
1345 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 return False
1347
1348 # List the commits inside the diff, and verify they are all local.
1349 commits_in_diff = RunGit(
1350 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1351 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1352 remote_branch = remote_branch.strip()
1353 if code != 0:
1354 _, remote_branch = self.GetRemoteBranch()
1355
1356 commits_in_remote = RunGit(
1357 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1358
1359 common_commits = set(commits_in_diff) & set(commits_in_remote)
1360 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001361 print('ERROR: Your diff contains %d commits already in %s.\n'
1362 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1363 'the diff. If you are using a custom git flow, you can override'
1364 ' the reference used for this check with "git config '
1365 'gitcl.remotebranch <git-ref>".' % (
1366 len(common_commits), remote_branch, upstream_git_obj),
1367 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001368 return False
1369 return True
1370
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001371 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001372 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001373
1374 Returns None if it is not set.
1375 """
tandrii5d48c322016-08-18 16:19:37 -07001376 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001377
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001378 def GetGitSvnRemoteUrl(self):
1379 """Return the configured git-svn remote URL parsed from git svn info.
1380
1381 Returns None if it is not set.
1382 """
1383 # URL is dependent on the current directory.
1384 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1385 if data:
1386 keys = dict(line.split(': ', 1) for line in data.splitlines()
1387 if ': ' in line)
1388 return keys.get('URL', None)
1389 return None
1390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391 def GetRemoteUrl(self):
1392 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1393
1394 Returns None if there is no remote.
1395 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001396 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001397 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1398
1399 # If URL is pointing to a local directory, it is probably a git cache.
1400 if os.path.isdir(url):
1401 url = RunGit(['config', 'remote.%s.url' % remote],
1402 error_ok=True,
1403 cwd=url).strip()
1404 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001406 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001407 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001408 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001409 self.issue = self._GitGetBranchConfigValue(
1410 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001411 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 return self.issue
1413
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 def GetIssueURL(self):
1415 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001416 issue = self.GetIssue()
1417 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001418 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001419 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420
1421 def GetDescription(self, pretty=False):
1422 if not self.has_description:
1423 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 self.has_description = True
1426 if pretty:
1427 wrapper = textwrap.TextWrapper()
1428 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1429 return wrapper.fill(self.description)
1430 return self.description
1431
1432 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001433 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001434 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001435 self.patchset = self._GitGetBranchConfigValue(
1436 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 return self.patchset
1439
1440 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001441 """Set this branch's patchset. If patchset=0, clears the patchset."""
1442 assert self.GetBranch()
1443 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001444 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001445 else:
1446 self.patchset = int(patchset)
1447 self._GitSetBranchConfigValue(
1448 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001450 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001451 """Set this branch's issue. If issue isn't given, clears the issue."""
1452 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001454 issue = int(issue)
1455 self._GitSetBranchConfigValue(
1456 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001457 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001458 codereview_server = self._codereview_impl.GetCodereviewServer()
1459 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.CodereviewServerConfigKey(),
1462 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 else:
tandrii5d48c322016-08-18 16:19:37 -07001464 # Reset all of these just to be clean.
1465 reset_suffixes = [
1466 'last-upload-hash',
1467 self._codereview_impl.IssueConfigKey(),
1468 self._codereview_impl.PatchsetConfigKey(),
1469 self._codereview_impl.CodereviewServerConfigKey(),
1470 ] + self._PostUnsetIssueProperties()
1471 for prop in reset_suffixes:
1472 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001473 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001474 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
dnjba1b0f32016-09-02 12:37:42 -07001476 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001477 if not self.GitSanityChecks(upstream_branch):
1478 DieWithError('\nGit sanity check failure')
1479
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001480 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001481 if not root:
1482 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001483 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001484
1485 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001487 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001488 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001489 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001490 except subprocess2.CalledProcessError:
1491 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001492 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 'This branch probably doesn\'t exist anymore. To reset the\n'
1494 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001495 ' git branch --set-upstream-to origin/master %s\n'
1496 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001497 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001498
maruel@chromium.org52424302012-08-29 15:14:30 +00001499 issue = self.GetIssue()
1500 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001501 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001502 description = self.GetDescription()
1503 else:
1504 # If the change was never uploaded, use the log messages of all commits
1505 # up to the branch point, as git cl upload will prefill the description
1506 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001507 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1508 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001509
1510 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001511 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001512 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001513 name,
1514 description,
1515 absroot,
1516 files,
1517 issue,
1518 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001519 author,
1520 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001521
dsansomee2d6fd92016-09-08 00:10:47 -07001522 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001524 return self._codereview_impl.UpdateDescriptionRemote(
1525 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526
1527 def RunHook(self, committing, may_prompt, verbose, change):
1528 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1529 try:
1530 return presubmit_support.DoPresubmitChecks(change, committing,
1531 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1532 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001533 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1534 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001535 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001536 DieWithError(
1537 ('%s\nMaybe your depot_tools is out of date?\n'
1538 'If all fails, contact maruel@') % e)
1539
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001540 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1541 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001542 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1543 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001544 else:
1545 # Assume url.
1546 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1547 urlparse.urlparse(issue_arg))
1548 if not parsed_issue_arg or not parsed_issue_arg.valid:
1549 DieWithError('Failed to parse issue argument "%s". '
1550 'Must be an issue number or a valid URL.' % issue_arg)
1551 return self._codereview_impl.CMDPatchWithParsedIssue(
1552 parsed_issue_arg, reject, nocommit, directory)
1553
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001554 def CMDUpload(self, options, git_diff_args, orig_args):
1555 """Uploads a change to codereview."""
1556 if git_diff_args:
1557 # TODO(ukai): is it ok for gerrit case?
1558 base_branch = git_diff_args[0]
1559 else:
1560 if self.GetBranch() is None:
1561 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1562
1563 # Default to diffing against common ancestor of upstream branch
1564 base_branch = self.GetCommonAncestorWithUpstream()
1565 git_diff_args = [base_branch, 'HEAD']
1566
1567 # Make sure authenticated to codereview before running potentially expensive
1568 # hooks. It is a fast, best efforts check. Codereview still can reject the
1569 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001570 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001571
1572 # Apply watchlists on upload.
1573 change = self.GetChange(base_branch, None)
1574 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1575 files = [f.LocalPath() for f in change.AffectedFiles()]
1576 if not options.bypass_watchlists:
1577 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1578
1579 if not options.bypass_hooks:
1580 if options.reviewers or options.tbr_owners:
1581 # Set the reviewer list now so that presubmit checks can access it.
1582 change_description = ChangeDescription(change.FullDescriptionText())
1583 change_description.update_reviewers(options.reviewers,
1584 options.tbr_owners,
1585 change)
1586 change.SetDescriptionText(change_description.description)
1587 hook_results = self.RunHook(committing=False,
1588 may_prompt=not options.force,
1589 verbose=options.verbose,
1590 change=change)
1591 if not hook_results.should_continue():
1592 return 1
1593 if not options.reviewers and hook_results.reviewers:
1594 options.reviewers = hook_results.reviewers.split(',')
1595
1596 if self.GetIssue():
1597 latest_patchset = self.GetMostRecentPatchset()
1598 local_patchset = self.GetPatchset()
1599 if (latest_patchset and local_patchset and
1600 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001601 print('The last upload made from this repository was patchset #%d but '
1602 'the most recent patchset on the server is #%d.'
1603 % (local_patchset, latest_patchset))
1604 print('Uploading will still work, but if you\'ve uploaded to this '
1605 'issue from another machine or branch the patch you\'re '
1606 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001607 ask_for_data('About to upload; enter to confirm.')
1608
1609 print_stats(options.similarity, options.find_copies, git_diff_args)
1610 ret = self.CMDUploadChange(options, git_diff_args, change)
1611 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001612 if options.use_commit_queue:
1613 self.SetCQState(_CQState.COMMIT)
1614 elif options.cq_dry_run:
1615 self.SetCQState(_CQState.DRY_RUN)
1616
tandrii5d48c322016-08-18 16:19:37 -07001617 _git_set_branch_config_value('last-upload-hash',
1618 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001619 # Run post upload hooks, if specified.
1620 if settings.GetRunPostUploadHook():
1621 presubmit_support.DoPostUploadExecuter(
1622 change,
1623 self,
1624 settings.GetRoot(),
1625 options.verbose,
1626 sys.stdout)
1627
1628 # Upload all dependencies if specified.
1629 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001630 print()
1631 print('--dependencies has been specified.')
1632 print('All dependent local branches will be re-uploaded.')
1633 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001634 # Remove the dependencies flag from args so that we do not end up in a
1635 # loop.
1636 orig_args.remove('--dependencies')
1637 ret = upload_branch_deps(self, orig_args)
1638 return ret
1639
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001640 def SetCQState(self, new_state):
1641 """Update the CQ state for latest patchset.
1642
1643 Issue must have been already uploaded and known.
1644 """
1645 assert new_state in _CQState.ALL_STATES
1646 assert self.GetIssue()
1647 return self._codereview_impl.SetCQState(new_state)
1648
qyearsley1fdfcb62016-10-24 13:22:03 -07001649 def TriggerDryRun(self):
1650 """Triggers a dry run and prints a warning on failure."""
1651 # TODO(qyearsley): Either re-use this method in CMDset_commit
1652 # and CMDupload, or change CMDtry to trigger dry runs with
1653 # just SetCQState, and catch keyboard interrupt and other
1654 # errors in that method.
1655 try:
1656 self.SetCQState(_CQState.DRY_RUN)
1657 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1658 return 0
1659 except KeyboardInterrupt:
1660 raise
1661 except:
1662 print('WARNING: failed to trigger CQ Dry Run.\n'
1663 'Either:\n'
1664 ' * your project has no CQ\n'
1665 ' * you don\'t have permission to trigger Dry Run\n'
1666 ' * bug in this code (see stack trace below).\n'
1667 'Consider specifying which bots to trigger manually '
1668 'or asking your project owners for permissions '
1669 'or contacting Chrome Infrastructure team at '
1670 'https://www.chromium.org/infra\n\n')
1671 # Still raise exception so that stack trace is printed.
1672 raise
1673
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 # Forward methods to codereview specific implementation.
1675
1676 def CloseIssue(self):
1677 return self._codereview_impl.CloseIssue()
1678
1679 def GetStatus(self):
1680 return self._codereview_impl.GetStatus()
1681
1682 def GetCodereviewServer(self):
1683 return self._codereview_impl.GetCodereviewServer()
1684
tandriide281ae2016-10-12 06:02:30 -07001685 def GetIssueOwner(self):
1686 """Get owner from codereview, which may differ from this checkout."""
1687 return self._codereview_impl.GetIssueOwner()
1688
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 def GetApprovingReviewers(self):
1690 return self._codereview_impl.GetApprovingReviewers()
1691
1692 def GetMostRecentPatchset(self):
1693 return self._codereview_impl.GetMostRecentPatchset()
1694
tandriide281ae2016-10-12 06:02:30 -07001695 def CannotTriggerTryJobReason(self):
1696 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1697 return self._codereview_impl.CannotTriggerTryJobReason()
1698
tandrii8c5a3532016-11-04 07:52:02 -07001699 def GetTryjobProperties(self, patchset=None):
1700 """Returns dictionary of properties to launch tryjob."""
1701 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1702
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001703 def __getattr__(self, attr):
1704 # This is because lots of untested code accesses Rietveld-specific stuff
1705 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001706 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001707 # Note that child method defines __getattr__ as well, and forwards it here,
1708 # because _RietveldChangelistImpl is not cleaned up yet, and given
1709 # deprecation of Rietveld, it should probably be just removed.
1710 # Until that time, avoid infinite recursion by bypassing __getattr__
1711 # of implementation class.
1712 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713
1714
1715class _ChangelistCodereviewBase(object):
1716 """Abstract base class encapsulating codereview specifics of a changelist."""
1717 def __init__(self, changelist):
1718 self._changelist = changelist # instance of Changelist
1719
1720 def __getattr__(self, attr):
1721 # Forward methods to changelist.
1722 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1723 # _RietveldChangelistImpl to avoid this hack?
1724 return getattr(self._changelist, attr)
1725
1726 def GetStatus(self):
1727 """Apply a rough heuristic to give a simple summary of an issue's review
1728 or CQ status, assuming adherence to a common workflow.
1729
1730 Returns None if no issue for this branch, or specific string keywords.
1731 """
1732 raise NotImplementedError()
1733
1734 def GetCodereviewServer(self):
1735 """Returns server URL without end slash, like "https://codereview.com"."""
1736 raise NotImplementedError()
1737
1738 def FetchDescription(self):
1739 """Fetches and returns description from the codereview server."""
1740 raise NotImplementedError()
1741
tandrii5d48c322016-08-18 16:19:37 -07001742 @classmethod
1743 def IssueConfigKey(cls):
1744 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001745 raise NotImplementedError()
1746
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001747 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001748 def PatchsetConfigKey(cls):
1749 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 raise NotImplementedError()
1751
tandrii5d48c322016-08-18 16:19:37 -07001752 @classmethod
1753 def CodereviewServerConfigKey(cls):
1754 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 raise NotImplementedError()
1756
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001757 def _PostUnsetIssueProperties(self):
1758 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001759 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001760
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 def GetRieveldObjForPresubmit(self):
1762 # This is an unfortunate Rietveld-embeddedness in presubmit.
1763 # For non-Rietveld codereviews, this probably should return a dummy object.
1764 raise NotImplementedError()
1765
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001766 def GetGerritObjForPresubmit(self):
1767 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1768 return None
1769
dsansomee2d6fd92016-09-08 00:10:47 -07001770 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 """Update the description on codereview site."""
1772 raise NotImplementedError()
1773
1774 def CloseIssue(self):
1775 """Closes the issue."""
1776 raise NotImplementedError()
1777
1778 def GetApprovingReviewers(self):
1779 """Returns a list of reviewers approving the change.
1780
1781 Note: not necessarily committers.
1782 """
1783 raise NotImplementedError()
1784
1785 def GetMostRecentPatchset(self):
1786 """Returns the most recent patchset number from the codereview site."""
1787 raise NotImplementedError()
1788
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001789 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1790 directory):
1791 """Fetches and applies the issue.
1792
1793 Arguments:
1794 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1795 reject: if True, reject the failed patch instead of switching to 3-way
1796 merge. Rietveld only.
1797 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1798 only.
1799 directory: switch to directory before applying the patch. Rietveld only.
1800 """
1801 raise NotImplementedError()
1802
1803 @staticmethod
1804 def ParseIssueURL(parsed_url):
1805 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1806 failed."""
1807 raise NotImplementedError()
1808
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001809 def EnsureAuthenticated(self, force):
1810 """Best effort check that user is authenticated with codereview server.
1811
1812 Arguments:
1813 force: whether to skip confirmation questions.
1814 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001815 raise NotImplementedError()
1816
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001817 def CMDUploadChange(self, options, args, change):
1818 """Uploads a change to codereview."""
1819 raise NotImplementedError()
1820
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001821 def SetCQState(self, new_state):
1822 """Update the CQ state for latest patchset.
1823
1824 Issue must have been already uploaded and known.
1825 """
1826 raise NotImplementedError()
1827
tandriie113dfd2016-10-11 10:20:12 -07001828 def CannotTriggerTryJobReason(self):
1829 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1830 raise NotImplementedError()
1831
tandriide281ae2016-10-12 06:02:30 -07001832 def GetIssueOwner(self):
1833 raise NotImplementedError()
1834
tandrii8c5a3532016-11-04 07:52:02 -07001835 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001836 raise NotImplementedError()
1837
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838
1839class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1840 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1841 super(_RietveldChangelistImpl, self).__init__(changelist)
1842 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001843 if not rietveld_server:
1844 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845
1846 self._rietveld_server = rietveld_server
1847 self._auth_config = auth_config
1848 self._props = None
1849 self._rpc_server = None
1850
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001851 def GetCodereviewServer(self):
1852 if not self._rietveld_server:
1853 # If we're on a branch then get the server potentially associated
1854 # with that branch.
1855 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001856 self._rietveld_server = gclient_utils.UpgradeToHttps(
1857 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 if not self._rietveld_server:
1859 self._rietveld_server = settings.GetDefaultServerUrl()
1860 return self._rietveld_server
1861
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001862 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001863 """Best effort check that user is authenticated with Rietveld server."""
1864 if self._auth_config.use_oauth2:
1865 authenticator = auth.get_authenticator_for_host(
1866 self.GetCodereviewServer(), self._auth_config)
1867 if not authenticator.has_cached_credentials():
1868 raise auth.LoginRequiredError(self.GetCodereviewServer())
1869
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001870 def FetchDescription(self):
1871 issue = self.GetIssue()
1872 assert issue
1873 try:
1874 return self.RpcServer().get_description(issue).strip()
1875 except urllib2.HTTPError as e:
1876 if e.code == 404:
1877 DieWithError(
1878 ('\nWhile fetching the description for issue %d, received a '
1879 '404 (not found)\n'
1880 'error. It is likely that you deleted this '
1881 'issue on the server. If this is the\n'
1882 'case, please run\n\n'
1883 ' git cl issue 0\n\n'
1884 'to clear the association with the deleted issue. Then run '
1885 'this command again.') % issue)
1886 else:
1887 DieWithError(
1888 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1889 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001890 print('Warning: Failed to retrieve CL description due to network '
1891 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001892 return ''
1893
1894 def GetMostRecentPatchset(self):
1895 return self.GetIssueProperties()['patchsets'][-1]
1896
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001897 def GetIssueProperties(self):
1898 if self._props is None:
1899 issue = self.GetIssue()
1900 if not issue:
1901 self._props = {}
1902 else:
1903 self._props = self.RpcServer().get_issue_properties(issue, True)
1904 return self._props
1905
tandriie113dfd2016-10-11 10:20:12 -07001906 def CannotTriggerTryJobReason(self):
1907 props = self.GetIssueProperties()
1908 if not props:
1909 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1910 if props.get('closed'):
1911 return 'CL %s is closed' % self.GetIssue()
1912 if props.get('private'):
1913 return 'CL %s is private' % self.GetIssue()
1914 return None
1915
tandrii8c5a3532016-11-04 07:52:02 -07001916 def GetTryjobProperties(self, patchset=None):
1917 """Returns dictionary of properties to launch tryjob."""
1918 project = (self.GetIssueProperties() or {}).get('project')
1919 return {
1920 'issue': self.GetIssue(),
1921 'patch_project': project,
1922 'patch_storage': 'rietveld',
1923 'patchset': patchset or self.GetPatchset(),
1924 'rietveld': self.GetCodereviewServer(),
1925 }
1926
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001927 def GetApprovingReviewers(self):
1928 return get_approving_reviewers(self.GetIssueProperties())
1929
tandriide281ae2016-10-12 06:02:30 -07001930 def GetIssueOwner(self):
1931 return (self.GetIssueProperties() or {}).get('owner_email')
1932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001933 def AddComment(self, message):
1934 return self.RpcServer().add_comment(self.GetIssue(), message)
1935
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001936 def GetStatus(self):
1937 """Apply a rough heuristic to give a simple summary of an issue's review
1938 or CQ status, assuming adherence to a common workflow.
1939
1940 Returns None if no issue for this branch, or one of the following keywords:
1941 * 'error' - error from review tool (including deleted issues)
1942 * 'unsent' - not sent for review
1943 * 'waiting' - waiting for review
1944 * 'reply' - waiting for owner to reply to review
1945 * 'lgtm' - LGTM from at least one approved reviewer
1946 * 'commit' - in the commit queue
1947 * 'closed' - closed
1948 """
1949 if not self.GetIssue():
1950 return None
1951
1952 try:
1953 props = self.GetIssueProperties()
1954 except urllib2.HTTPError:
1955 return 'error'
1956
1957 if props.get('closed'):
1958 # Issue is closed.
1959 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001960 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001961 # Issue is in the commit queue.
1962 return 'commit'
1963
1964 try:
1965 reviewers = self.GetApprovingReviewers()
1966 except urllib2.HTTPError:
1967 return 'error'
1968
1969 if reviewers:
1970 # Was LGTM'ed.
1971 return 'lgtm'
1972
1973 messages = props.get('messages') or []
1974
tandrii9d2c7a32016-06-22 03:42:45 -07001975 # Skip CQ messages that don't require owner's action.
1976 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1977 if 'Dry run:' in messages[-1]['text']:
1978 messages.pop()
1979 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1980 # This message always follows prior messages from CQ,
1981 # so skip this too.
1982 messages.pop()
1983 else:
1984 # This is probably a CQ messages warranting user attention.
1985 break
1986
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001987 if not messages:
1988 # No message was sent.
1989 return 'unsent'
1990 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001991 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001992 return 'reply'
1993 return 'waiting'
1994
dsansomee2d6fd92016-09-08 00:10:47 -07001995 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001996 return self.RpcServer().update_description(
1997 self.GetIssue(), self.description)
1998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002000 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002002 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002003 return self.SetFlags({flag: value})
2004
2005 def SetFlags(self, flags):
2006 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002007 """
phajdan.jr68598232016-08-10 03:28:28 -07002008 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002009 try:
tandrii4b233bd2016-07-06 03:50:29 -07002010 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002011 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002012 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002013 if e.code == 404:
2014 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2015 if e.code == 403:
2016 DieWithError(
2017 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002018 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002019 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002021 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022 """Returns an upload.RpcServer() to access this review's rietveld instance.
2023 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002024 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002025 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002026 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002027 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002028 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002030 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002031 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002032 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
tandrii5d48c322016-08-18 16:19:37 -07002034 @classmethod
2035 def PatchsetConfigKey(cls):
2036 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037
tandrii5d48c322016-08-18 16:19:37 -07002038 @classmethod
2039 def CodereviewServerConfigKey(cls):
2040 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002041
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002042 def GetRieveldObjForPresubmit(self):
2043 return self.RpcServer()
2044
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002045 def SetCQState(self, new_state):
2046 props = self.GetIssueProperties()
2047 if props.get('private'):
2048 DieWithError('Cannot set-commit on private issue')
2049
2050 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002051 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002052 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002053 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002054 else:
tandrii4b233bd2016-07-06 03:50:29 -07002055 assert new_state == _CQState.DRY_RUN
2056 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002057
2058
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002059 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2060 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002061 # PatchIssue should never be called with a dirty tree. It is up to the
2062 # caller to check this, but just in case we assert here since the
2063 # consequences of the caller not checking this could be dire.
2064 assert(not git_common.is_dirty_git_tree('apply'))
2065 assert(parsed_issue_arg.valid)
2066 self._changelist.issue = parsed_issue_arg.issue
2067 if parsed_issue_arg.hostname:
2068 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2069
skobes6468b902016-10-24 08:45:10 -07002070 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2071 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2072 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002073 try:
skobes6468b902016-10-24 08:45:10 -07002074 scm_obj.apply_patch(patchset_object)
2075 except Exception as e:
2076 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002077 return 1
2078
2079 # If we had an issue, commit the current state and register the issue.
2080 if not nocommit:
2081 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2082 'patch from issue %(i)s at patchset '
2083 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2084 % {'i': self.GetIssue(), 'p': patchset})])
2085 self.SetIssue(self.GetIssue())
2086 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002087 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002088 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002089 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002090 return 0
2091
2092 @staticmethod
2093 def ParseIssueURL(parsed_url):
2094 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2095 return None
wychen3c1c1722016-08-04 11:46:36 -07002096 # Rietveld patch: https://domain/<number>/#ps<patchset>
2097 match = re.match(r'/(\d+)/$', parsed_url.path)
2098 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2099 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002100 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002101 issue=int(match.group(1)),
2102 patchset=int(match2.group(1)),
2103 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002104 # Typical url: https://domain/<issue_number>[/[other]]
2105 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2106 if match:
skobes6468b902016-10-24 08:45:10 -07002107 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002108 issue=int(match.group(1)),
2109 hostname=parsed_url.netloc)
2110 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2111 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2112 if match:
skobes6468b902016-10-24 08:45:10 -07002113 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 issue=int(match.group(1)),
2115 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002116 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002117 return None
2118
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002119 def CMDUploadChange(self, options, args, change):
2120 """Upload the patch to Rietveld."""
2121 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2122 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002123 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2124 if options.emulate_svn_auto_props:
2125 upload_args.append('--emulate_svn_auto_props')
2126
2127 change_desc = None
2128
2129 if options.email is not None:
2130 upload_args.extend(['--email', options.email])
2131
2132 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002133 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002134 upload_args.extend(['--title', options.title])
2135 if options.message:
2136 upload_args.extend(['--message', options.message])
2137 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002138 print('This branch is associated with issue %s. '
2139 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002140 else:
nodirca166002016-06-27 10:59:51 -07002141 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002142 upload_args.extend(['--title', options.title])
2143 message = (options.title or options.message or
2144 CreateDescriptionFromLog(args))
2145 change_desc = ChangeDescription(message)
2146 if options.reviewers or options.tbr_owners:
2147 change_desc.update_reviewers(options.reviewers,
2148 options.tbr_owners,
2149 change)
2150 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002151 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002152
2153 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002154 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155 return 1
2156
2157 upload_args.extend(['--message', change_desc.description])
2158 if change_desc.get_reviewers():
2159 upload_args.append('--reviewers=%s' % ','.join(
2160 change_desc.get_reviewers()))
2161 if options.send_mail:
2162 if not change_desc.get_reviewers():
2163 DieWithError("Must specify reviewers to send email.")
2164 upload_args.append('--send_mail')
2165
2166 # We check this before applying rietveld.private assuming that in
2167 # rietveld.cc only addresses which we can send private CLs to are listed
2168 # if rietveld.private is set, and so we should ignore rietveld.cc only
2169 # when --private is specified explicitly on the command line.
2170 if options.private:
2171 logging.warn('rietveld.cc is ignored since private flag is specified. '
2172 'You need to review and add them manually if necessary.')
2173 cc = self.GetCCListWithoutDefault()
2174 else:
2175 cc = self.GetCCList()
2176 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002177 if change_desc.get_cced():
2178 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002179 if cc:
2180 upload_args.extend(['--cc', cc])
2181
2182 if options.private or settings.GetDefaultPrivateFlag() == "True":
2183 upload_args.append('--private')
2184
2185 upload_args.extend(['--git_similarity', str(options.similarity)])
2186 if not options.find_copies:
2187 upload_args.extend(['--git_no_find_copies'])
2188
2189 # Include the upstream repo's URL in the change -- this is useful for
2190 # projects that have their source spread across multiple repos.
2191 remote_url = self.GetGitBaseUrlFromConfig()
2192 if not remote_url:
2193 if settings.GetIsGitSvn():
2194 remote_url = self.GetGitSvnRemoteUrl()
2195 else:
2196 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2197 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2198 self.GetUpstreamBranch().split('/')[-1])
2199 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200 remote, remote_branch = self.GetRemoteBranch()
2201 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2202 settings.GetPendingRefPrefix())
2203 if target_ref:
2204 upload_args.extend(['--target_ref', target_ref])
2205
2206 # Look for dependent patchsets. See crbug.com/480453 for more details.
2207 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2208 upstream_branch = ShortBranchName(upstream_branch)
2209 if remote is '.':
2210 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002211 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002212 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002213 print()
2214 print('Skipping dependency patchset upload because git config '
2215 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2216 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002217 else:
2218 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002219 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 auth_config=auth_config)
2221 branch_cl_issue_url = branch_cl.GetIssueURL()
2222 branch_cl_issue = branch_cl.GetIssue()
2223 branch_cl_patchset = branch_cl.GetPatchset()
2224 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2225 upload_args.extend(
2226 ['--depends_on_patchset', '%s:%s' % (
2227 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002228 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 '\n'
2230 'The current branch (%s) is tracking a local branch (%s) with '
2231 'an associated CL.\n'
2232 'Adding %s/#ps%s as a dependency patchset.\n'
2233 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2234 branch_cl_patchset))
2235
2236 project = settings.GetProject()
2237 if project:
2238 upload_args.extend(['--project', project])
2239
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 try:
2241 upload_args = ['upload'] + upload_args + args
2242 logging.info('upload.RealMain(%s)', upload_args)
2243 issue, patchset = upload.RealMain(upload_args)
2244 issue = int(issue)
2245 patchset = int(patchset)
2246 except KeyboardInterrupt:
2247 sys.exit(1)
2248 except:
2249 # If we got an exception after the user typed a description for their
2250 # change, back up the description before re-raising.
2251 if change_desc:
2252 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2253 print('\nGot exception while uploading -- saving description to %s\n' %
2254 backup_path)
2255 backup_file = open(backup_path, 'w')
2256 backup_file.write(change_desc.description)
2257 backup_file.close()
2258 raise
2259
2260 if not self.GetIssue():
2261 self.SetIssue(issue)
2262 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002263 return 0
2264
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002265
2266class _GerritChangelistImpl(_ChangelistCodereviewBase):
2267 def __init__(self, changelist, auth_config=None):
2268 # auth_config is Rietveld thing, kept here to preserve interface only.
2269 super(_GerritChangelistImpl, self).__init__(changelist)
2270 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002271 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002272 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002273 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002274
2275 def _GetGerritHost(self):
2276 # Lazy load of configs.
2277 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002278 if self._gerrit_host and '.' not in self._gerrit_host:
2279 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2280 # This happens for internal stuff http://crbug.com/614312.
2281 parsed = urlparse.urlparse(self.GetRemoteUrl())
2282 if parsed.scheme == 'sso':
2283 print('WARNING: using non https URLs for remote is likely broken\n'
2284 ' Your current remote is: %s' % self.GetRemoteUrl())
2285 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2286 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002287 return self._gerrit_host
2288
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002289 def _GetGitHost(self):
2290 """Returns git host to be used when uploading change to Gerrit."""
2291 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2292
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002293 def GetCodereviewServer(self):
2294 if not self._gerrit_server:
2295 # If we're on a branch then get the server potentially associated
2296 # with that branch.
2297 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002298 self._gerrit_server = self._GitGetBranchConfigValue(
2299 self.CodereviewServerConfigKey())
2300 if self._gerrit_server:
2301 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002302 if not self._gerrit_server:
2303 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2304 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002305 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306 parts[0] = parts[0] + '-review'
2307 self._gerrit_host = '.'.join(parts)
2308 self._gerrit_server = 'https://%s' % self._gerrit_host
2309 return self._gerrit_server
2310
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002311 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002312 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002313 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002314
tandrii5d48c322016-08-18 16:19:37 -07002315 @classmethod
2316 def PatchsetConfigKey(cls):
2317 return 'gerritpatchset'
2318
2319 @classmethod
2320 def CodereviewServerConfigKey(cls):
2321 return 'gerritserver'
2322
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002323 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002324 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002325 if settings.GetGerritSkipEnsureAuthenticated():
2326 # For projects with unusual authentication schemes.
2327 # See http://crbug.com/603378.
2328 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002329 # Lazy-loader to identify Gerrit and Git hosts.
2330 if gerrit_util.GceAuthenticator.is_gce():
2331 return
2332 self.GetCodereviewServer()
2333 git_host = self._GetGitHost()
2334 assert self._gerrit_server and self._gerrit_host
2335 cookie_auth = gerrit_util.CookiesAuthenticator()
2336
2337 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2338 git_auth = cookie_auth.get_auth_header(git_host)
2339 if gerrit_auth and git_auth:
2340 if gerrit_auth == git_auth:
2341 return
2342 print((
2343 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2344 ' Check your %s or %s file for credentials of hosts:\n'
2345 ' %s\n'
2346 ' %s\n'
2347 ' %s') %
2348 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2349 git_host, self._gerrit_host,
2350 cookie_auth.get_new_password_message(git_host)))
2351 if not force:
2352 ask_for_data('If you know what you are doing, press Enter to continue, '
2353 'Ctrl+C to abort.')
2354 return
2355 else:
2356 missing = (
2357 [] if gerrit_auth else [self._gerrit_host] +
2358 [] if git_auth else [git_host])
2359 DieWithError('Credentials for the following hosts are required:\n'
2360 ' %s\n'
2361 'These are read from %s (or legacy %s)\n'
2362 '%s' % (
2363 '\n '.join(missing),
2364 cookie_auth.get_gitcookies_path(),
2365 cookie_auth.get_netrc_path(),
2366 cookie_auth.get_new_password_message(git_host)))
2367
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002368 def _PostUnsetIssueProperties(self):
2369 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002370 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002371
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002372 def GetRieveldObjForPresubmit(self):
2373 class ThisIsNotRietveldIssue(object):
2374 def __nonzero__(self):
2375 # This is a hack to make presubmit_support think that rietveld is not
2376 # defined, yet still ensure that calls directly result in a decent
2377 # exception message below.
2378 return False
2379
2380 def __getattr__(self, attr):
2381 print(
2382 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2383 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2384 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2385 'or use Rietveld for codereview.\n'
2386 'See also http://crbug.com/579160.' % attr)
2387 raise NotImplementedError()
2388 return ThisIsNotRietveldIssue()
2389
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002390 def GetGerritObjForPresubmit(self):
2391 return presubmit_support.GerritAccessor(self._GetGerritHost())
2392
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002393 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002394 """Apply a rough heuristic to give a simple summary of an issue's review
2395 or CQ status, assuming adherence to a common workflow.
2396
2397 Returns None if no issue for this branch, or one of the following keywords:
2398 * 'error' - error from review tool (including deleted issues)
2399 * 'unsent' - no reviewers added
2400 * 'waiting' - waiting for review
2401 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002402 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2403 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002404 * 'commit' - in the commit queue
2405 * 'closed' - abandoned
2406 """
2407 if not self.GetIssue():
2408 return None
2409
2410 try:
2411 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002412 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002413 return 'error'
2414
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002415 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002416 return 'closed'
2417
2418 cq_label = data['labels'].get('Commit-Queue', {})
2419 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002420 votes = cq_label.get('all', [])
2421 highest_vote = 0
2422 for v in votes:
2423 highest_vote = max(highest_vote, v.get('value', 0))
2424 vote_value = str(highest_vote)
2425 if vote_value != '0':
2426 # Add a '+' if the value is not 0 to match the values in the label.
2427 # The cq_label does not have negatives.
2428 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002429 vote_text = cq_label.get('values', {}).get(vote_value, '')
2430 if vote_text.lower() == 'commit':
2431 return 'commit'
2432
2433 lgtm_label = data['labels'].get('Code-Review', {})
2434 if lgtm_label:
2435 if 'rejected' in lgtm_label:
2436 return 'not lgtm'
2437 if 'approved' in lgtm_label:
2438 return 'lgtm'
2439
2440 if not data.get('reviewers', {}).get('REVIEWER', []):
2441 return 'unsent'
2442
2443 messages = data.get('messages', [])
2444 if messages:
2445 owner = data['owner'].get('_account_id')
2446 last_message_author = messages[-1].get('author', {}).get('_account_id')
2447 if owner != last_message_author:
2448 # Some reply from non-owner.
2449 return 'reply'
2450
2451 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002452
2453 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002454 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002455 return data['revisions'][data['current_revision']]['_number']
2456
2457 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002458 data = self._GetChangeDetail(['CURRENT_REVISION'])
2459 current_rev = data['current_revision']
2460 url = data['revisions'][current_rev]['fetch']['http']['url']
2461 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002462
dsansomee2d6fd92016-09-08 00:10:47 -07002463 def UpdateDescriptionRemote(self, description, force=False):
2464 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2465 if not force:
2466 ask_for_data(
2467 'The description cannot be modified while the issue has a pending '
2468 'unpublished edit. Either publish the edit in the Gerrit web UI '
2469 'or delete it.\n\n'
2470 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2471
2472 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2473 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002474 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2475 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002476
2477 def CloseIssue(self):
2478 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2479
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002480 def GetApprovingReviewers(self):
2481 """Returns a list of reviewers approving the change.
2482
2483 Note: not necessarily committers.
2484 """
2485 raise NotImplementedError()
2486
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002487 def SubmitIssue(self, wait_for_merge=True):
2488 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2489 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002490
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002491 def _GetChangeDetail(self, options=None, issue=None):
2492 options = options or []
2493 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002494 assert issue, 'issue is required to query Gerrit'
2495 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002496 options)
tandriic2405f52016-10-10 08:13:15 -07002497 if not data:
2498 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2499 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002500
agable32978d92016-11-01 12:55:02 -07002501 def _GetChangeCommit(self, issue=None):
2502 issue = issue or self.GetIssue()
2503 assert issue, 'issue is required to query Gerrit'
2504 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2505 if not data:
2506 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2507 return data
2508
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002509 def CMDLand(self, force, bypass_hooks, verbose):
2510 if git_common.is_dirty_git_tree('land'):
2511 return 1
tandriid60367b2016-06-22 05:25:12 -07002512 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2513 if u'Commit-Queue' in detail.get('labels', {}):
2514 if not force:
2515 ask_for_data('\nIt seems this repository has a Commit Queue, '
2516 'which can test and land changes for you. '
2517 'Are you sure you wish to bypass it?\n'
2518 'Press Enter to continue, Ctrl+C to abort.')
2519
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520 differs = True
tandriic4344b52016-08-29 06:04:54 -07002521 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002522 # Note: git diff outputs nothing if there is no diff.
2523 if not last_upload or RunGit(['diff', last_upload]).strip():
2524 print('WARNING: some changes from local branch haven\'t been uploaded')
2525 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002526 if detail['current_revision'] == last_upload:
2527 differs = False
2528 else:
2529 print('WARNING: local branch contents differ from latest uploaded '
2530 'patchset')
2531 if differs:
2532 if not force:
2533 ask_for_data(
2534 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2535 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2536 elif not bypass_hooks:
2537 hook_results = self.RunHook(
2538 committing=True,
2539 may_prompt=not force,
2540 verbose=verbose,
2541 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2542 if not hook_results.should_continue():
2543 return 1
2544
2545 self.SubmitIssue(wait_for_merge=True)
2546 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002547 links = self._GetChangeCommit().get('web_links', [])
2548 for link in links:
2549 if link.get('name') == 'gerrit' and link.get('url'):
2550 print('Landed as %s' % link.get('url'))
2551 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002552 return 0
2553
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002554 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2555 directory):
2556 assert not reject
2557 assert not nocommit
2558 assert not directory
2559 assert parsed_issue_arg.valid
2560
2561 self._changelist.issue = parsed_issue_arg.issue
2562
2563 if parsed_issue_arg.hostname:
2564 self._gerrit_host = parsed_issue_arg.hostname
2565 self._gerrit_server = 'https://%s' % self._gerrit_host
2566
tandriic2405f52016-10-10 08:13:15 -07002567 try:
2568 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2569 except GerritIssueNotExists as e:
2570 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002571
2572 if not parsed_issue_arg.patchset:
2573 # Use current revision by default.
2574 revision_info = detail['revisions'][detail['current_revision']]
2575 patchset = int(revision_info['_number'])
2576 else:
2577 patchset = parsed_issue_arg.patchset
2578 for revision_info in detail['revisions'].itervalues():
2579 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2580 break
2581 else:
2582 DieWithError('Couldn\'t find patchset %i in issue %i' %
2583 (parsed_issue_arg.patchset, self.GetIssue()))
2584
2585 fetch_info = revision_info['fetch']['http']
2586 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2587 RunGit(['cherry-pick', 'FETCH_HEAD'])
2588 self.SetIssue(self.GetIssue())
2589 self.SetPatchset(patchset)
2590 print('Committed patch for issue %i pathset %i locally' %
2591 (self.GetIssue(), self.GetPatchset()))
2592 return 0
2593
2594 @staticmethod
2595 def ParseIssueURL(parsed_url):
2596 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2597 return None
2598 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2599 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2600 # Short urls like https://domain/<issue_number> can be used, but don't allow
2601 # specifying the patchset (you'd 404), but we allow that here.
2602 if parsed_url.path == '/':
2603 part = parsed_url.fragment
2604 else:
2605 part = parsed_url.path
2606 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2607 if match:
2608 return _ParsedIssueNumberArgument(
2609 issue=int(match.group(2)),
2610 patchset=int(match.group(4)) if match.group(4) else None,
2611 hostname=parsed_url.netloc)
2612 return None
2613
tandrii16e0b4e2016-06-07 10:34:28 -07002614 def _GerritCommitMsgHookCheck(self, offer_removal):
2615 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2616 if not os.path.exists(hook):
2617 return
2618 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2619 # custom developer made one.
2620 data = gclient_utils.FileRead(hook)
2621 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2622 return
2623 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002624 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002625 'and may interfere with it in subtle ways.\n'
2626 'We recommend you remove the commit-msg hook.')
2627 if offer_removal:
2628 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2629 if reply.lower().startswith('y'):
2630 gclient_utils.rm_file_or_tree(hook)
2631 print('Gerrit commit-msg hook removed.')
2632 else:
2633 print('OK, will keep Gerrit commit-msg hook in place.')
2634
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002635 def CMDUploadChange(self, options, args, change):
2636 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002637 if options.squash and options.no_squash:
2638 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002639
2640 if not options.squash and not options.no_squash:
2641 # Load default for user, repo, squash=true, in this order.
2642 options.squash = settings.GetSquashGerritUploads()
2643 elif options.no_squash:
2644 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002645
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002646 # We assume the remote called "origin" is the one we want.
2647 # It is probably not worthwhile to support different workflows.
2648 gerrit_remote = 'origin'
2649
2650 remote, remote_branch = self.GetRemoteBranch()
2651 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2652 pending_prefix='')
2653
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002654 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002655 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002656 if self.GetIssue():
2657 # Try to get the message from a previous upload.
2658 message = self.GetDescription()
2659 if not message:
2660 DieWithError(
2661 'failed to fetch description from current Gerrit issue %d\n'
2662 '%s' % (self.GetIssue(), self.GetIssueURL()))
2663 change_id = self._GetChangeDetail()['change_id']
2664 while True:
2665 footer_change_ids = git_footers.get_footer_change_id(message)
2666 if footer_change_ids == [change_id]:
2667 break
2668 if not footer_change_ids:
2669 message = git_footers.add_footer_change_id(message, change_id)
2670 print('WARNING: appended missing Change-Id to issue description')
2671 continue
2672 # There is already a valid footer but with different or several ids.
2673 # Doing this automatically is non-trivial as we don't want to lose
2674 # existing other footers, yet we want to append just 1 desired
2675 # Change-Id. Thus, just create a new footer, but let user verify the
2676 # new description.
2677 message = '%s\n\nChange-Id: %s' % (message, change_id)
2678 print(
2679 'WARNING: issue %s has Change-Id footer(s):\n'
2680 ' %s\n'
2681 'but issue has Change-Id %s, according to Gerrit.\n'
2682 'Please, check the proposed correction to the description, '
2683 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2684 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2685 change_id))
2686 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2687 if not options.force:
2688 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002689 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002690 message = change_desc.description
2691 if not message:
2692 DieWithError("Description is empty. Aborting...")
2693 # Continue the while loop.
2694 # Sanity check of this code - we should end up with proper message
2695 # footer.
2696 assert [change_id] == git_footers.get_footer_change_id(message)
2697 change_desc = ChangeDescription(message)
2698 else:
2699 change_desc = ChangeDescription(
2700 options.message or CreateDescriptionFromLog(args))
2701 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002702 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 if not change_desc.description:
2704 DieWithError("Description is empty. Aborting...")
2705 message = change_desc.description
2706 change_ids = git_footers.get_footer_change_id(message)
2707 if len(change_ids) > 1:
2708 DieWithError('too many Change-Id footers, at most 1 allowed.')
2709 if not change_ids:
2710 # Generate the Change-Id automatically.
2711 message = git_footers.add_footer_change_id(
2712 message, GenerateGerritChangeId(message))
2713 change_desc.set_description(message)
2714 change_ids = git_footers.get_footer_change_id(message)
2715 assert len(change_ids) == 1
2716 change_id = change_ids[0]
2717
2718 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2719 if remote is '.':
2720 # If our upstream branch is local, we base our squashed commit on its
2721 # squashed version.
2722 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2723 # Check the squashed hash of the parent.
2724 parent = RunGit(['config',
2725 'branch.%s.gerritsquashhash' % upstream_branch_name],
2726 error_ok=True).strip()
2727 # Verify that the upstream branch has been uploaded too, otherwise
2728 # Gerrit will create additional CLs when uploading.
2729 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2730 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 DieWithError(
2732 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002733 'Note: maybe you\'ve uploaded it with --no-squash. '
2734 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735 ' git cl upload --squash\n' % upstream_branch_name)
2736 else:
2737 parent = self.GetCommonAncestorWithUpstream()
2738
2739 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2740 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2741 '-m', message]).strip()
2742 else:
2743 change_desc = ChangeDescription(
2744 options.message or CreateDescriptionFromLog(args))
2745 if not change_desc.description:
2746 DieWithError("Description is empty. Aborting...")
2747
2748 if not git_footers.get_footer_change_id(change_desc.description):
2749 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002750 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2751 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002752 ref_to_push = 'HEAD'
2753 parent = '%s/%s' % (gerrit_remote, branch)
2754 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2755
2756 assert change_desc
2757 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2758 ref_to_push)]).splitlines()
2759 if len(commits) > 1:
2760 print('WARNING: This will upload %d commits. Run the following command '
2761 'to see which commits will be uploaded: ' % len(commits))
2762 print('git log %s..%s' % (parent, ref_to_push))
2763 print('You can also use `git squash-branch` to squash these into a '
2764 'single commit.')
2765 ask_for_data('About to upload; enter to confirm.')
2766
2767 if options.reviewers or options.tbr_owners:
2768 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2769 change)
2770
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002771 # Extra options that can be specified at push time. Doc:
2772 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2773 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002774 if change_desc.get_reviewers(tbr_only=True):
2775 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2776 refspec_opts.append('l=Code-Review+1')
2777
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002778 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002779 if not re.match(r'^[\w ]+$', options.title):
2780 options.title = re.sub(r'[^\w ]', '', options.title)
2781 print('WARNING: Patchset title may only contain alphanumeric chars '
2782 'and spaces. Cleaned up title:\n%s' % options.title)
2783 if not options.force:
2784 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002785 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2786 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002787 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2788
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002789 if options.send_mail:
2790 if not change_desc.get_reviewers():
2791 DieWithError('Must specify reviewers to send email.')
2792 refspec_opts.append('notify=ALL')
2793 else:
2794 refspec_opts.append('notify=NONE')
2795
tandrii99a72f22016-08-17 14:33:24 -07002796 reviewers = change_desc.get_reviewers()
2797 if reviewers:
2798 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002799
agablec6787972016-09-09 16:13:34 -07002800 if options.private:
2801 refspec_opts.append('draft')
2802
rmistry9eadede2016-09-19 11:22:43 -07002803 if options.topic:
2804 # Documentation on Gerrit topics is here:
2805 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2806 refspec_opts.append('topic=%s' % options.topic)
2807
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002808 refspec_suffix = ''
2809 if refspec_opts:
2810 refspec_suffix = '%' + ','.join(refspec_opts)
2811 assert ' ' not in refspec_suffix, (
2812 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002814
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002815 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002816 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002817 print_stdout=True,
2818 # Flush after every line: useful for seeing progress when running as
2819 # recipe.
2820 filter_fn=lambda _: sys.stdout.flush())
2821
2822 if options.squash:
2823 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2824 change_numbers = [m.group(1)
2825 for m in map(regex.match, push_stdout.splitlines())
2826 if m]
2827 if len(change_numbers) != 1:
2828 DieWithError(
2829 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2830 'Change-Id: %s') % (len(change_numbers), change_id))
2831 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002832 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002833
2834 # Add cc's from the CC_LIST and --cc flag (if any).
2835 cc = self.GetCCList().split(',')
2836 if options.cc:
2837 cc.extend(options.cc)
2838 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002839 if change_desc.get_cced():
2840 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002841 if cc:
2842 gerrit_util.AddReviewers(
2843 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2844
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002845 return 0
2846
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002847 def _AddChangeIdToCommitMessage(self, options, args):
2848 """Re-commits using the current message, assumes the commit hook is in
2849 place.
2850 """
2851 log_desc = options.message or CreateDescriptionFromLog(args)
2852 git_command = ['commit', '--amend', '-m', log_desc]
2853 RunGit(git_command)
2854 new_log_desc = CreateDescriptionFromLog(args)
2855 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002856 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002857 return new_log_desc
2858 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002859 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002860
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002861 def SetCQState(self, new_state):
2862 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002863 vote_map = {
2864 _CQState.NONE: 0,
2865 _CQState.DRY_RUN: 1,
2866 _CQState.COMMIT : 2,
2867 }
2868 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2869 labels={'Commit-Queue': vote_map[new_state]})
2870
tandriie113dfd2016-10-11 10:20:12 -07002871 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002872 try:
2873 data = self._GetChangeDetail()
2874 except GerritIssueNotExists:
2875 return 'Gerrit doesn\'t know about your issue %s' % self.GetIssue()
2876
2877 if data['status'] in ('ABANDONED', 'MERGED'):
2878 return 'CL %s is closed' % self.GetIssue()
2879
2880 def GetTryjobProperties(self, patchset=None):
2881 """Returns dictionary of properties to launch tryjob."""
2882 data = self._GetChangeDetail(['ALL_REVISIONS'])
2883 patchset = int(patchset or self.GetPatchset())
2884 assert patchset
2885 revision_data = None # Pylint wants it to be defined.
2886 for revision_data in data['revisions'].itervalues():
2887 if int(revision_data['_number']) == patchset:
2888 break
2889 else:
2890 raise Exception('Patchset %d is not known in Gerrit issue %d' %
2891 (patchset, self.GetIssue()))
2892 return {
2893 'patch_issue': self.GetIssue(),
2894 'patch_set': patchset or self.GetPatchset(),
2895 'patch_project': data['project'],
2896 'patch_storage': 'gerrit',
2897 'patch_ref': revision_data['fetch']['http']['ref'],
2898 'patch_repository_url': revision_data['fetch']['http']['url'],
2899 'patch_gerrit_url': self.GetCodereviewServer(),
2900 }
tandriie113dfd2016-10-11 10:20:12 -07002901
tandriide281ae2016-10-12 06:02:30 -07002902 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002903 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002904
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002905
2906_CODEREVIEW_IMPLEMENTATIONS = {
2907 'rietveld': _RietveldChangelistImpl,
2908 'gerrit': _GerritChangelistImpl,
2909}
2910
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002911
iannuccie53c9352016-08-17 14:40:40 -07002912def _add_codereview_issue_select_options(parser, extra=""):
2913 _add_codereview_select_options(parser)
2914
2915 text = ('Operate on this issue number instead of the current branch\'s '
2916 'implicit issue.')
2917 if extra:
2918 text += ' '+extra
2919 parser.add_option('-i', '--issue', type=int, help=text)
2920
2921
2922def _process_codereview_issue_select_options(parser, options):
2923 _process_codereview_select_options(parser, options)
2924 if options.issue is not None and not options.forced_codereview:
2925 parser.error('--issue must be specified with either --rietveld or --gerrit')
2926
2927
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002928def _add_codereview_select_options(parser):
2929 """Appends --gerrit and --rietveld options to force specific codereview."""
2930 parser.codereview_group = optparse.OptionGroup(
2931 parser, 'EXPERIMENTAL! Codereview override options')
2932 parser.add_option_group(parser.codereview_group)
2933 parser.codereview_group.add_option(
2934 '--gerrit', action='store_true',
2935 help='Force the use of Gerrit for codereview')
2936 parser.codereview_group.add_option(
2937 '--rietveld', action='store_true',
2938 help='Force the use of Rietveld for codereview')
2939
2940
2941def _process_codereview_select_options(parser, options):
2942 if options.gerrit and options.rietveld:
2943 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2944 options.forced_codereview = None
2945 if options.gerrit:
2946 options.forced_codereview = 'gerrit'
2947 elif options.rietveld:
2948 options.forced_codereview = 'rietveld'
2949
2950
tandriif9aefb72016-07-01 09:06:51 -07002951def _get_bug_line_values(default_project, bugs):
2952 """Given default_project and comma separated list of bugs, yields bug line
2953 values.
2954
2955 Each bug can be either:
2956 * a number, which is combined with default_project
2957 * string, which is left as is.
2958
2959 This function may produce more than one line, because bugdroid expects one
2960 project per line.
2961
2962 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2963 ['v8:123', 'chromium:789']
2964 """
2965 default_bugs = []
2966 others = []
2967 for bug in bugs.split(','):
2968 bug = bug.strip()
2969 if bug:
2970 try:
2971 default_bugs.append(int(bug))
2972 except ValueError:
2973 others.append(bug)
2974
2975 if default_bugs:
2976 default_bugs = ','.join(map(str, default_bugs))
2977 if default_project:
2978 yield '%s:%s' % (default_project, default_bugs)
2979 else:
2980 yield default_bugs
2981 for other in sorted(others):
2982 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2983 yield other
2984
2985
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002986class ChangeDescription(object):
2987 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002988 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002989 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002990 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002991
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002992 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002993 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002994
agable@chromium.org42c20792013-09-12 17:34:49 +00002995 @property # www.logilab.org/ticket/89786
2996 def description(self): # pylint: disable=E0202
2997 return '\n'.join(self._description_lines)
2998
2999 def set_description(self, desc):
3000 if isinstance(desc, basestring):
3001 lines = desc.splitlines()
3002 else:
3003 lines = [line.rstrip() for line in desc]
3004 while lines and not lines[0]:
3005 lines.pop(0)
3006 while lines and not lines[-1]:
3007 lines.pop(-1)
3008 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003009
piman@chromium.org336f9122014-09-04 02:16:55 +00003010 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003011 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003012 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003013 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003014 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003015 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003016
agable@chromium.org42c20792013-09-12 17:34:49 +00003017 # Get the set of R= and TBR= lines and remove them from the desciption.
3018 regexp = re.compile(self.R_LINE)
3019 matches = [regexp.match(line) for line in self._description_lines]
3020 new_desc = [l for i, l in enumerate(self._description_lines)
3021 if not matches[i]]
3022 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003023
agable@chromium.org42c20792013-09-12 17:34:49 +00003024 # Construct new unified R= and TBR= lines.
3025 r_names = []
3026 tbr_names = []
3027 for match in matches:
3028 if not match:
3029 continue
3030 people = cleanup_list([match.group(2).strip()])
3031 if match.group(1) == 'TBR':
3032 tbr_names.extend(people)
3033 else:
3034 r_names.extend(people)
3035 for name in r_names:
3036 if name not in reviewers:
3037 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003038 if add_owners_tbr:
3039 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003040 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003041 all_reviewers = set(tbr_names + reviewers)
3042 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3043 all_reviewers)
3044 tbr_names.extend(owners_db.reviewers_for(missing_files,
3045 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003046 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3047 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3048
3049 # Put the new lines in the description where the old first R= line was.
3050 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3051 if 0 <= line_loc < len(self._description_lines):
3052 if new_tbr_line:
3053 self._description_lines.insert(line_loc, new_tbr_line)
3054 if new_r_line:
3055 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003056 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003057 if new_r_line:
3058 self.append_footer(new_r_line)
3059 if new_tbr_line:
3060 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003061
tandriif9aefb72016-07-01 09:06:51 -07003062 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003063 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 self.set_description([
3065 '# Enter a description of the change.',
3066 '# This will be displayed on the codereview site.',
3067 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003068 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003069 '--------------------',
3070 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003071
agable@chromium.org42c20792013-09-12 17:34:49 +00003072 regexp = re.compile(self.BUG_LINE)
3073 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003074 prefix = settings.GetBugPrefix()
3075 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3076 for value in values:
3077 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3078 self.append_footer('BUG=%s' % value)
3079
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003081 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003082 if not content:
3083 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003085
3086 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003087 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3088 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003089 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003091
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003093 """Adds a footer line to the description.
3094
3095 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3096 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3097 that Gerrit footers are always at the end.
3098 """
3099 parsed_footer_line = git_footers.parse_footer(line)
3100 if parsed_footer_line:
3101 # Line is a gerrit footer in the form: Footer-Key: any value.
3102 # Thus, must be appended observing Gerrit footer rules.
3103 self.set_description(
3104 git_footers.add_footer(self.description,
3105 key=parsed_footer_line[0],
3106 value=parsed_footer_line[1]))
3107 return
3108
3109 if not self._description_lines:
3110 self._description_lines.append(line)
3111 return
3112
3113 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3114 if gerrit_footers:
3115 # git_footers.split_footers ensures that there is an empty line before
3116 # actual (gerrit) footers, if any. We have to keep it that way.
3117 assert top_lines and top_lines[-1] == ''
3118 top_lines, separator = top_lines[:-1], top_lines[-1:]
3119 else:
3120 separator = [] # No need for separator if there are no gerrit_footers.
3121
3122 prev_line = top_lines[-1] if top_lines else ''
3123 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3124 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3125 top_lines.append('')
3126 top_lines.append(line)
3127 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003128
tandrii99a72f22016-08-17 14:33:24 -07003129 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003130 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003131 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003132 reviewers = [match.group(2).strip()
3133 for match in matches
3134 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003135 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003136
bradnelsond975b302016-10-23 12:20:23 -07003137 def get_cced(self):
3138 """Retrieves the list of reviewers."""
3139 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3140 cced = [match.group(2).strip() for match in matches if match]
3141 return cleanup_list(cced)
3142
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003143
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003144def get_approving_reviewers(props):
3145 """Retrieves the reviewers that approved a CL from the issue properties with
3146 messages.
3147
3148 Note that the list may contain reviewers that are not committer, thus are not
3149 considered by the CQ.
3150 """
3151 return sorted(
3152 set(
3153 message['sender']
3154 for message in props['messages']
3155 if message['approval'] and message['sender'] in props['reviewers']
3156 )
3157 )
3158
3159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003160def FindCodereviewSettingsFile(filename='codereview.settings'):
3161 """Finds the given file starting in the cwd and going up.
3162
3163 Only looks up to the top of the repository unless an
3164 'inherit-review-settings-ok' file exists in the root of the repository.
3165 """
3166 inherit_ok_file = 'inherit-review-settings-ok'
3167 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003168 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3170 root = '/'
3171 while True:
3172 if filename in os.listdir(cwd):
3173 if os.path.isfile(os.path.join(cwd, filename)):
3174 return open(os.path.join(cwd, filename))
3175 if cwd == root:
3176 break
3177 cwd = os.path.dirname(cwd)
3178
3179
3180def LoadCodereviewSettingsFromFile(fileobj):
3181 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003182 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003184 def SetProperty(name, setting, unset_error_ok=False):
3185 fullname = 'rietveld.' + name
3186 if setting in keyvals:
3187 RunGit(['config', fullname, keyvals[setting]])
3188 else:
3189 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3190
tandrii48df5812016-10-17 03:55:37 -07003191 if not keyvals.get('GERRIT_HOST', False):
3192 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003193 # Only server setting is required. Other settings can be absent.
3194 # In that case, we ignore errors raised during option deletion attempt.
3195 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003196 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003197 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3198 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003199 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003200 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003201 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3202 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003203 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003204 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003205 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003206 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003207 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3208 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003209
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003210 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003211 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003212
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003213 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003214 RunGit(['config', 'gerrit.squash-uploads',
3215 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003216
tandrii@chromium.org28253532016-04-14 13:46:56 +00003217 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003218 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003219 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3222 #should be of the form
3223 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3224 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3225 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3226 keyvals['ORIGIN_URL_CONFIG']])
3227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003229def urlretrieve(source, destination):
3230 """urllib is broken for SSL connections via a proxy therefore we
3231 can't use urllib.urlretrieve()."""
3232 with open(destination, 'w') as f:
3233 f.write(urllib2.urlopen(source).read())
3234
3235
ukai@chromium.org712d6102013-11-27 00:52:58 +00003236def hasSheBang(fname):
3237 """Checks fname is a #! script."""
3238 with open(fname) as f:
3239 return f.read(2).startswith('#!')
3240
3241
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003242# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3243def DownloadHooks(*args, **kwargs):
3244 pass
3245
3246
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003247def DownloadGerritHook(force):
3248 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003249
3250 Args:
3251 force: True to update hooks. False to install hooks if not present.
3252 """
3253 if not settings.GetIsGerrit():
3254 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003255 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003256 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3257 if not os.access(dst, os.X_OK):
3258 if os.path.exists(dst):
3259 if not force:
3260 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003261 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003262 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003263 if not hasSheBang(dst):
3264 DieWithError('Not a script: %s\n'
3265 'You need to download from\n%s\n'
3266 'into .git/hooks/commit-msg and '
3267 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003268 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3269 except Exception:
3270 if os.path.exists(dst):
3271 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003272 DieWithError('\nFailed to download hooks.\n'
3273 'You need to download from\n%s\n'
3274 'into .git/hooks/commit-msg and '
3275 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003276
3277
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003278
3279def GetRietveldCodereviewSettingsInteractively():
3280 """Prompt the user for settings."""
3281 server = settings.GetDefaultServerUrl(error_ok=True)
3282 prompt = 'Rietveld server (host[:port])'
3283 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3284 newserver = ask_for_data(prompt + ':')
3285 if not server and not newserver:
3286 newserver = DEFAULT_SERVER
3287 if newserver:
3288 newserver = gclient_utils.UpgradeToHttps(newserver)
3289 if newserver != server:
3290 RunGit(['config', 'rietveld.server', newserver])
3291
3292 def SetProperty(initial, caption, name, is_url):
3293 prompt = caption
3294 if initial:
3295 prompt += ' ("x" to clear) [%s]' % initial
3296 new_val = ask_for_data(prompt + ':')
3297 if new_val == 'x':
3298 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3299 elif new_val:
3300 if is_url:
3301 new_val = gclient_utils.UpgradeToHttps(new_val)
3302 if new_val != initial:
3303 RunGit(['config', 'rietveld.' + name, new_val])
3304
3305 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3306 SetProperty(settings.GetDefaultPrivateFlag(),
3307 'Private flag (rietveld only)', 'private', False)
3308 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3309 'tree-status-url', False)
3310 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3311 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3312 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3313 'run-post-upload-hook', False)
3314
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003315@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003317 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318
tandrii5d0a0422016-09-14 06:24:35 -07003319 print('WARNING: git cl config works for Rietveld only')
3320 # TODO(tandrii): remove this once we switch to Gerrit.
3321 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003322 parser.add_option('--activate-update', action='store_true',
3323 help='activate auto-updating [rietveld] section in '
3324 '.git/config')
3325 parser.add_option('--deactivate-update', action='store_true',
3326 help='deactivate auto-updating [rietveld] section in '
3327 '.git/config')
3328 options, args = parser.parse_args(args)
3329
3330 if options.deactivate_update:
3331 RunGit(['config', 'rietveld.autoupdate', 'false'])
3332 return
3333
3334 if options.activate_update:
3335 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3336 return
3337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003339 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340 return 0
3341
3342 url = args[0]
3343 if not url.endswith('codereview.settings'):
3344 url = os.path.join(url, 'codereview.settings')
3345
3346 # Load code review settings and download hooks (if available).
3347 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3348 return 0
3349
3350
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003351def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003352 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003353 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3354 branch = ShortBranchName(branchref)
3355 _, args = parser.parse_args(args)
3356 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003357 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003358 return RunGit(['config', 'branch.%s.base-url' % branch],
3359 error_ok=False).strip()
3360 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003361 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003362 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3363 error_ok=False).strip()
3364
3365
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003366def color_for_status(status):
3367 """Maps a Changelist status to color, for CMDstatus and other tools."""
3368 return {
3369 'unsent': Fore.RED,
3370 'waiting': Fore.BLUE,
3371 'reply': Fore.YELLOW,
3372 'lgtm': Fore.GREEN,
3373 'commit': Fore.MAGENTA,
3374 'closed': Fore.CYAN,
3375 'error': Fore.WHITE,
3376 }.get(status, Fore.WHITE)
3377
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003378
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003379def get_cl_statuses(changes, fine_grained, max_processes=None):
3380 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003381
3382 If fine_grained is true, this will fetch CL statuses from the server.
3383 Otherwise, simply indicate if there's a matching url for the given branches.
3384
3385 If max_processes is specified, it is used as the maximum number of processes
3386 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3387 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003388
3389 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003390 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003391 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003392 upload.verbosity = 0
3393
3394 if fine_grained:
3395 # Process one branch synchronously to work through authentication, then
3396 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003397 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003398 def fetch(cl):
3399 try:
3400 return (cl, cl.GetStatus())
3401 except:
3402 # See http://crbug.com/629863.
3403 logging.exception('failed to fetch status for %s:', cl)
3404 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003405 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003406
tandriiea9514a2016-08-17 12:32:37 -07003407 changes_to_fetch = changes[1:]
3408 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003409 # Exit early if there was only one branch to fetch.
3410 return
3411
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003412 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003413 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003414 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003415 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003416
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003417 fetched_cls = set()
3418 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003419 while True:
3420 try:
3421 row = it.next(timeout=5)
3422 except multiprocessing.TimeoutError:
3423 break
3424
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003425 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003426 yield row
3427
3428 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003429 for cl in set(changes_to_fetch) - fetched_cls:
3430 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003431
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003432 else:
3433 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003434 for cl in changes:
3435 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003436
rmistry@google.com2dd99862015-06-22 12:22:18 +00003437
3438def upload_branch_deps(cl, args):
3439 """Uploads CLs of local branches that are dependents of the current branch.
3440
3441 If the local branch dependency tree looks like:
3442 test1 -> test2.1 -> test3.1
3443 -> test3.2
3444 -> test2.2 -> test3.3
3445
3446 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3447 run on the dependent branches in this order:
3448 test2.1, test3.1, test3.2, test2.2, test3.3
3449
3450 Note: This function does not rebase your local dependent branches. Use it when
3451 you make a change to the parent branch that will not conflict with its
3452 dependent branches, and you would like their dependencies updated in
3453 Rietveld.
3454 """
3455 if git_common.is_dirty_git_tree('upload-branch-deps'):
3456 return 1
3457
3458 root_branch = cl.GetBranch()
3459 if root_branch is None:
3460 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3461 'Get on a branch!')
3462 if not cl.GetIssue() or not cl.GetPatchset():
3463 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3464 'patchset dependencies without an uploaded CL.')
3465
3466 branches = RunGit(['for-each-ref',
3467 '--format=%(refname:short) %(upstream:short)',
3468 'refs/heads'])
3469 if not branches:
3470 print('No local branches found.')
3471 return 0
3472
3473 # Create a dictionary of all local branches to the branches that are dependent
3474 # on it.
3475 tracked_to_dependents = collections.defaultdict(list)
3476 for b in branches.splitlines():
3477 tokens = b.split()
3478 if len(tokens) == 2:
3479 branch_name, tracked = tokens
3480 tracked_to_dependents[tracked].append(branch_name)
3481
vapiera7fbd5a2016-06-16 09:17:49 -07003482 print()
3483 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003484 dependents = []
3485 def traverse_dependents_preorder(branch, padding=''):
3486 dependents_to_process = tracked_to_dependents.get(branch, [])
3487 padding += ' '
3488 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003489 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003490 dependents.append(dependent)
3491 traverse_dependents_preorder(dependent, padding)
3492 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003493 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003494
3495 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003496 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003497 return 0
3498
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print('This command will checkout all dependent branches and run '
3500 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003501 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3502
andybons@chromium.org962f9462016-02-03 20:00:42 +00003503 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003504 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003505 args.extend(['-t', 'Updated patchset dependency'])
3506
rmistry@google.com2dd99862015-06-22 12:22:18 +00003507 # Record all dependents that failed to upload.
3508 failures = {}
3509 # Go through all dependents, checkout the branch and upload.
3510 try:
3511 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003512 print()
3513 print('--------------------------------------')
3514 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003515 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003516 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003517 try:
3518 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003519 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003520 failures[dependent_branch] = 1
3521 except: # pylint: disable=W0702
3522 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003523 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003524 finally:
3525 # Swap back to the original root branch.
3526 RunGit(['checkout', '-q', root_branch])
3527
vapiera7fbd5a2016-06-16 09:17:49 -07003528 print()
3529 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003530 for dependent_branch in dependents:
3531 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003532 print(' %s : %s' % (dependent_branch, upload_status))
3533 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003534
3535 return 0
3536
3537
kmarshall3bff56b2016-06-06 18:31:47 -07003538def CMDarchive(parser, args):
3539 """Archives and deletes branches associated with closed changelists."""
3540 parser.add_option(
3541 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003542 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003543 parser.add_option(
3544 '-f', '--force', action='store_true',
3545 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003546 parser.add_option(
3547 '-d', '--dry-run', action='store_true',
3548 help='Skip the branch tagging and removal steps.')
3549 parser.add_option(
3550 '-t', '--notags', action='store_true',
3551 help='Do not tag archived branches. '
3552 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003553
3554 auth.add_auth_options(parser)
3555 options, args = parser.parse_args(args)
3556 if args:
3557 parser.error('Unsupported args: %s' % ' '.join(args))
3558 auth_config = auth.extract_auth_config_from_options(options)
3559
3560 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3561 if not branches:
3562 return 0
3563
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003565 changes = [Changelist(branchref=b, auth_config=auth_config)
3566 for b in branches.splitlines()]
3567 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3568 statuses = get_cl_statuses(changes,
3569 fine_grained=True,
3570 max_processes=options.maxjobs)
3571 proposal = [(cl.GetBranch(),
3572 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3573 for cl, status in statuses
3574 if status == 'closed']
3575 proposal.sort()
3576
3577 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003579 return 0
3580
3581 current_branch = GetCurrentBranch()
3582
vapiera7fbd5a2016-06-16 09:17:49 -07003583 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003584 if options.notags:
3585 for next_item in proposal:
3586 print(' ' + next_item[0])
3587 else:
3588 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3589 for next_item in proposal:
3590 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003591
kmarshall9249e012016-08-23 12:02:16 -07003592 # Quit now on precondition failure or if instructed by the user, either
3593 # via an interactive prompt or by command line flags.
3594 if options.dry_run:
3595 print('\nNo changes were made (dry run).\n')
3596 return 0
3597 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003598 print('You are currently on a branch \'%s\' which is associated with a '
3599 'closed codereview issue, so archive cannot proceed. Please '
3600 'checkout another branch and run this command again.' %
3601 current_branch)
3602 return 1
kmarshall9249e012016-08-23 12:02:16 -07003603 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003604 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3605 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003606 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003607 return 1
3608
3609 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003610 if not options.notags:
3611 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003612 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003613
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003615
3616 return 0
3617
3618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003620 """Show status of changelists.
3621
3622 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003623 - Red not sent for review or broken
3624 - Blue waiting for review
3625 - Yellow waiting for you to reply to review
3626 - Green LGTM'ed
3627 - Magenta in the commit queue
3628 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003629
3630 Also see 'git cl comments'.
3631 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003633 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003634 parser.add_option('-f', '--fast', action='store_true',
3635 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003636 parser.add_option(
3637 '-j', '--maxjobs', action='store', type=int,
3638 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003639
3640 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003641 _add_codereview_issue_select_options(
3642 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003643 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003644 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003645 if args:
3646 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003647 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648
iannuccie53c9352016-08-17 14:40:40 -07003649 if options.issue is not None and not options.field:
3650 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003652 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003653 cl = Changelist(auth_config=auth_config, issue=options.issue,
3654 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003655 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003656 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657 elif options.field == 'id':
3658 issueid = cl.GetIssue()
3659 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003660 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661 elif options.field == 'patch':
3662 patchset = cl.GetPatchset()
3663 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003665 elif options.field == 'status':
3666 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003667 elif options.field == 'url':
3668 url = cl.GetIssueURL()
3669 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003670 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003671 return 0
3672
3673 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3674 if not branches:
3675 print('No local branch found.')
3676 return 0
3677
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003678 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003679 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003680 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003681 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003682 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003683 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003684 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003685
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003686 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003687 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3688 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3689 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003690 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003691 c, status = output.next()
3692 branch_statuses[c.GetBranch()] = status
3693 status = branch_statuses.pop(branch)
3694 url = cl.GetIssueURL()
3695 if url and (not status or status == 'error'):
3696 # The issue probably doesn't exist anymore.
3697 url += ' (broken)'
3698
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003699 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003700 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003701 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003702 color = ''
3703 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003704 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003705 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003706 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003707 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003708
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003710 print()
3711 print('Current branch:',)
3712 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003713 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003714 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003715 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003716 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003717 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print('Issue description:')
3719 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003720 return 0
3721
3722
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003723def colorize_CMDstatus_doc():
3724 """To be called once in main() to add colors to git cl status help."""
3725 colors = [i for i in dir(Fore) if i[0].isupper()]
3726
3727 def colorize_line(line):
3728 for color in colors:
3729 if color in line.upper():
3730 # Extract whitespaces first and the leading '-'.
3731 indent = len(line) - len(line.lstrip(' ')) + 1
3732 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3733 return line
3734
3735 lines = CMDstatus.__doc__.splitlines()
3736 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3737
3738
phajdan.jre328cf92016-08-22 04:12:17 -07003739def write_json(path, contents):
3740 with open(path, 'w') as f:
3741 json.dump(contents, f)
3742
3743
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003744@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003745def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003746 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003747
3748 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003749 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003750 parser.add_option('-r', '--reverse', action='store_true',
3751 help='Lookup the branch(es) for the specified issues. If '
3752 'no issues are specified, all branches with mapped '
3753 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003754 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003755 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003756 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003757 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758
dnj@chromium.org406c4402015-03-03 17:22:28 +00003759 if options.reverse:
3760 branches = RunGit(['for-each-ref', 'refs/heads',
3761 '--format=%(refname:short)']).splitlines()
3762
3763 # Reverse issue lookup.
3764 issue_branch_map = {}
3765 for branch in branches:
3766 cl = Changelist(branchref=branch)
3767 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3768 if not args:
3769 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003770 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003771 for issue in args:
3772 if not issue:
3773 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003774 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003775 print('Branch for issue number %s: %s' % (
3776 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003777 if options.json:
3778 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003779 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003780 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003781 if len(args) > 0:
3782 try:
3783 issue = int(args[0])
3784 except ValueError:
3785 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003786 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003787 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003789 if options.json:
3790 write_json(options.json, {
3791 'issue': cl.GetIssue(),
3792 'issue_url': cl.GetIssueURL(),
3793 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003794 return 0
3795
3796
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003797def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003798 """Shows or posts review comments for any changelist."""
3799 parser.add_option('-a', '--add-comment', dest='comment',
3800 help='comment to add to an issue')
3801 parser.add_option('-i', dest='issue',
3802 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003803 parser.add_option('-j', '--json-file',
3804 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003805 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003806 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003807 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003808
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003809 issue = None
3810 if options.issue:
3811 try:
3812 issue = int(options.issue)
3813 except ValueError:
3814 DieWithError('A review issue id is expected to be a number')
3815
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003816 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003817
3818 if options.comment:
3819 cl.AddComment(options.comment)
3820 return 0
3821
3822 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003823 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003824 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003825 summary.append({
3826 'date': message['date'],
3827 'lgtm': False,
3828 'message': message['text'],
3829 'not_lgtm': False,
3830 'sender': message['sender'],
3831 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003832 if message['disapproval']:
3833 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003834 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003835 elif message['approval']:
3836 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003837 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003838 elif message['sender'] == data['owner_email']:
3839 color = Fore.MAGENTA
3840 else:
3841 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003842 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003843 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003844 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003845 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003847 if options.json_file:
3848 with open(options.json_file, 'wb') as f:
3849 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003850 return 0
3851
3852
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003853@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003854def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003855 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003856 parser.add_option('-d', '--display', action='store_true',
3857 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003858 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003859 help='New description to set for this issue (- for stdin, '
3860 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003861 parser.add_option('-f', '--force', action='store_true',
3862 help='Delete any unpublished Gerrit edits for this issue '
3863 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003864
3865 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003866 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003867 options, args = parser.parse_args(args)
3868 _process_codereview_select_options(parser, options)
3869
3870 target_issue = None
3871 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003872 target_issue = ParseIssueNumberArgument(args[0])
3873 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003874 parser.print_help()
3875 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003876
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003877 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003878
martiniss6eda05f2016-06-30 10:18:35 -07003879 kwargs = {
3880 'auth_config': auth_config,
3881 'codereview': options.forced_codereview,
3882 }
3883 if target_issue:
3884 kwargs['issue'] = target_issue.issue
3885 if options.forced_codereview == 'rietveld':
3886 kwargs['rietveld_server'] = target_issue.hostname
3887
3888 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003889
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003890 if not cl.GetIssue():
3891 DieWithError('This branch has no associated changelist.')
3892 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003893
smut@google.com34fb6b12015-07-13 20:03:26 +00003894 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003895 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003896 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003897
3898 if options.new_description:
3899 text = options.new_description
3900 if text == '-':
3901 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003902 elif text == '+':
3903 base_branch = cl.GetCommonAncestorWithUpstream()
3904 change = cl.GetChange(base_branch, None, local_description=True)
3905 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003906
3907 description.set_description(text)
3908 else:
3909 description.prompt()
3910
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003911 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003912 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003913 return 0
3914
3915
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916def CreateDescriptionFromLog(args):
3917 """Pulls out the commit log to use as a base for the CL description."""
3918 log_args = []
3919 if len(args) == 1 and not args[0].endswith('.'):
3920 log_args = [args[0] + '..']
3921 elif len(args) == 1 and args[0].endswith('...'):
3922 log_args = [args[0][:-1]]
3923 elif len(args) == 2:
3924 log_args = [args[0] + '..' + args[1]]
3925 else:
3926 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003927 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928
3929
thestig@chromium.org44202a22014-03-11 19:22:18 +00003930def CMDlint(parser, args):
3931 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003932 parser.add_option('--filter', action='append', metavar='-x,+y',
3933 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003934 auth.add_auth_options(parser)
3935 options, args = parser.parse_args(args)
3936 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003937
3938 # Access to a protected member _XX of a client class
3939 # pylint: disable=W0212
3940 try:
3941 import cpplint
3942 import cpplint_chromium
3943 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003944 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003945 return 1
3946
3947 # Change the current working directory before calling lint so that it
3948 # shows the correct base.
3949 previous_cwd = os.getcwd()
3950 os.chdir(settings.GetRoot())
3951 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003952 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003953 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3954 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003955 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003956 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003957 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003958
3959 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003960 command = args + files
3961 if options.filter:
3962 command = ['--filter=' + ','.join(options.filter)] + command
3963 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003964
3965 white_regex = re.compile(settings.GetLintRegex())
3966 black_regex = re.compile(settings.GetLintIgnoreRegex())
3967 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3968 for filename in filenames:
3969 if white_regex.match(filename):
3970 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003971 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003972 else:
3973 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3974 extra_check_functions)
3975 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003976 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003977 finally:
3978 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003979 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003980 if cpplint._cpplint_state.error_count != 0:
3981 return 1
3982 return 0
3983
3984
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003985def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003986 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003987 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003989 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003990 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003991 auth.add_auth_options(parser)
3992 options, args = parser.parse_args(args)
3993 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994
sbc@chromium.org71437c02015-04-09 19:29:40 +00003995 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003996 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997 return 1
3998
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003999 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 if args:
4001 base_branch = args[0]
4002 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004003 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004004 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004006 cl.RunHook(
4007 committing=not options.upload,
4008 may_prompt=False,
4009 verbose=options.verbose,
4010 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004011 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004012
4013
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004014def GenerateGerritChangeId(message):
4015 """Returns Ixxxxxx...xxx change id.
4016
4017 Works the same way as
4018 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4019 but can be called on demand on all platforms.
4020
4021 The basic idea is to generate git hash of a state of the tree, original commit
4022 message, author/committer info and timestamps.
4023 """
4024 lines = []
4025 tree_hash = RunGitSilent(['write-tree'])
4026 lines.append('tree %s' % tree_hash.strip())
4027 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4028 if code == 0:
4029 lines.append('parent %s' % parent.strip())
4030 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4031 lines.append('author %s' % author.strip())
4032 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4033 lines.append('committer %s' % committer.strip())
4034 lines.append('')
4035 # Note: Gerrit's commit-hook actually cleans message of some lines and
4036 # whitespace. This code is not doing this, but it clearly won't decrease
4037 # entropy.
4038 lines.append(message)
4039 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4040 stdin='\n'.join(lines))
4041 return 'I%s' % change_hash.strip()
4042
4043
wittman@chromium.org455dc922015-01-26 20:15:50 +00004044def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4045 """Computes the remote branch ref to use for the CL.
4046
4047 Args:
4048 remote (str): The git remote for the CL.
4049 remote_branch (str): The git remote branch for the CL.
4050 target_branch (str): The target branch specified by the user.
4051 pending_prefix (str): The pending prefix from the settings.
4052 """
4053 if not (remote and remote_branch):
4054 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004055
wittman@chromium.org455dc922015-01-26 20:15:50 +00004056 if target_branch:
4057 # Cannonicalize branch references to the equivalent local full symbolic
4058 # refs, which are then translated into the remote full symbolic refs
4059 # below.
4060 if '/' not in target_branch:
4061 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4062 else:
4063 prefix_replacements = (
4064 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4065 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4066 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4067 )
4068 match = None
4069 for regex, replacement in prefix_replacements:
4070 match = re.search(regex, target_branch)
4071 if match:
4072 remote_branch = target_branch.replace(match.group(0), replacement)
4073 break
4074 if not match:
4075 # This is a branch path but not one we recognize; use as-is.
4076 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004077 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4078 # Handle the refs that need to land in different refs.
4079 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004080
wittman@chromium.org455dc922015-01-26 20:15:50 +00004081 # Create the true path to the remote branch.
4082 # Does the following translation:
4083 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4084 # * refs/remotes/origin/master -> refs/heads/master
4085 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4086 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4087 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4088 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4089 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4090 'refs/heads/')
4091 elif remote_branch.startswith('refs/remotes/branch-heads'):
4092 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4093 # If a pending prefix exists then replace refs/ with it.
4094 if pending_prefix:
4095 remote_branch = remote_branch.replace('refs/', pending_prefix)
4096 return remote_branch
4097
4098
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004099def cleanup_list(l):
4100 """Fixes a list so that comma separated items are put as individual items.
4101
4102 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4103 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4104 """
4105 items = sum((i.split(',') for i in l), [])
4106 stripped_items = (i.strip() for i in items)
4107 return sorted(filter(None, stripped_items))
4108
4109
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004110@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004111def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004112 """Uploads the current changelist to codereview.
4113
4114 Can skip dependency patchset uploads for a branch by running:
4115 git config branch.branch_name.skip-deps-uploads True
4116 To unset run:
4117 git config --unset branch.branch_name.skip-deps-uploads
4118 Can also set the above globally by using the --global flag.
4119 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004120 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4121 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004122 parser.add_option('--bypass-watchlists', action='store_true',
4123 dest='bypass_watchlists',
4124 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004125 parser.add_option('-f', action='store_true', dest='force',
4126 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004127 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004128 parser.add_option('-b', '--bug',
4129 help='pre-populate the bug number(s) for this issue. '
4130 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004131 parser.add_option('--message-file', dest='message_file',
4132 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004133 parser.add_option('-t', dest='title',
4134 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004135 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004136 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004137 help='reviewer email addresses')
4138 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004139 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004140 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004141 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004142 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004143 parser.add_option('--emulate_svn_auto_props',
4144 '--emulate-svn-auto-props',
4145 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004146 dest="emulate_svn_auto_props",
4147 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004148 parser.add_option('-c', '--use-commit-queue', action='store_true',
4149 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004150 parser.add_option('--private', action='store_true',
4151 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004152 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004153 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004154 metavar='TARGET',
4155 help='Apply CL to remote ref TARGET. ' +
4156 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004157 parser.add_option('--squash', action='store_true',
4158 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004159 parser.add_option('--no-squash', action='store_true',
4160 help='Don\'t squash multiple commits into one ' +
4161 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004162 parser.add_option('--topic', default=None,
4163 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004164 parser.add_option('--email', default=None,
4165 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004166 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4167 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004168 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4169 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004170 help='Send the patchset to do a CQ dry run right after '
4171 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004172 parser.add_option('--dependencies', action='store_true',
4173 help='Uploads CLs of all the local branches that depend on '
4174 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004175
rmistry@google.com2dd99862015-06-22 12:22:18 +00004176 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004177 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004178 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004179 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004180 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004181 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004182 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004183
sbc@chromium.org71437c02015-04-09 19:29:40 +00004184 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004185 return 1
4186
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004187 options.reviewers = cleanup_list(options.reviewers)
4188 options.cc = cleanup_list(options.cc)
4189
tandriib80458a2016-06-23 12:20:07 -07004190 if options.message_file:
4191 if options.message:
4192 parser.error('only one of --message and --message-file allowed.')
4193 options.message = gclient_utils.FileRead(options.message_file)
4194 options.message_file = None
4195
tandrii4d0545a2016-07-06 03:56:49 -07004196 if options.cq_dry_run and options.use_commit_queue:
4197 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4198
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004199 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4200 settings.GetIsGerrit()
4201
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004202 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004203 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004204
4205
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004206def IsSubmoduleMergeCommit(ref):
4207 # When submodules are added to the repo, we expect there to be a single
4208 # non-git-svn merge commit at remote HEAD with a signature comment.
4209 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004210 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004211 return RunGit(cmd) != ''
4212
4213
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004214def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004215 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004217 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4218 upstream and closes the issue automatically and atomically.
4219
4220 Otherwise (in case of Rietveld):
4221 Squashes branch into a single commit.
4222 Updates changelog with metadata (e.g. pointer to review).
4223 Pushes/dcommits the code upstream.
4224 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004225 """
4226 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4227 help='bypass upload presubmit hook')
4228 parser.add_option('-m', dest='message',
4229 help="override review description")
4230 parser.add_option('-f', action='store_true', dest='force',
4231 help="force yes to questions (don't prompt)")
4232 parser.add_option('-c', dest='contributor',
4233 help="external contributor for patch (appended to " +
4234 "description and used as author for git). Should be " +
4235 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004236 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004238 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004239 auth_config = auth.extract_auth_config_from_options(options)
4240
4241 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004242
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004243 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4244 if cl.IsGerrit():
4245 if options.message:
4246 # This could be implemented, but it requires sending a new patch to
4247 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4248 # Besides, Gerrit has the ability to change the commit message on submit
4249 # automatically, thus there is no need to support this option (so far?).
4250 parser.error('-m MESSAGE option is not supported for Gerrit.')
4251 if options.contributor:
4252 parser.error(
4253 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4254 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4255 'the contributor\'s "name <email>". If you can\'t upload such a '
4256 'commit for review, contact your repository admin and request'
4257 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004258 if not cl.GetIssue():
4259 DieWithError('You must upload the issue first to Gerrit.\n'
4260 ' If you would rather have `git cl land` upload '
4261 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004262 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4263 options.verbose)
4264
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004265 current = cl.GetBranch()
4266 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4267 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004268 print()
4269 print('Attempting to push branch %r into another local branch!' % current)
4270 print()
4271 print('Either reparent this branch on top of origin/master:')
4272 print(' git reparent-branch --root')
4273 print()
4274 print('OR run `git rebase-update` if you think the parent branch is ')
4275 print('already committed.')
4276 print()
4277 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004278 return 1
4279
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004280 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281 # Default to merging against our best guess of the upstream branch.
4282 args = [cl.GetUpstreamBranch()]
4283
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004284 if options.contributor:
4285 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004286 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004287 return 1
4288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004289 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004290 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004291
sbc@chromium.org71437c02015-04-09 19:29:40 +00004292 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293 return 1
4294
4295 # This rev-list syntax means "show all commits not in my branch that
4296 # are in base_branch".
4297 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4298 base_branch]).splitlines()
4299 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004300 print('Base branch "%s" has %d commits '
4301 'not in this branch.' % (base_branch, len(upstream_commits)))
4302 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303 return 1
4304
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004305 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004306 svn_head = None
4307 if cmd == 'dcommit' or base_has_submodules:
4308 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4309 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004312 # If the base_head is a submodule merge commit, the first parent of the
4313 # base_head should be a git-svn commit, which is what we're interested in.
4314 base_svn_head = base_branch
4315 if base_has_submodules:
4316 base_svn_head += '^1'
4317
4318 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004319 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004320 print('This branch has %d additional commits not upstreamed yet.'
4321 % len(extra_commits.splitlines()))
4322 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4323 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004324 return 1
4325
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004326 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004327 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004328 author = None
4329 if options.contributor:
4330 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004331 hook_results = cl.RunHook(
4332 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004333 may_prompt=not options.force,
4334 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004335 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004336 if not hook_results.should_continue():
4337 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004338
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004339 # Check the tree status if the tree status URL is set.
4340 status = GetTreeStatus()
4341 if 'closed' == status:
4342 print('The tree is closed. Please wait for it to reopen. Use '
4343 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4344 return 1
4345 elif 'unknown' == status:
4346 print('Unable to determine tree status. Please verify manually and '
4347 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4348 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004350 change_desc = ChangeDescription(options.message)
4351 if not change_desc.description and cl.GetIssue():
4352 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004353
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004354 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004355 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004356 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004357 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004358 print('No description set.')
4359 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004360 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004361
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004362 # Keep a separate copy for the commit message, because the commit message
4363 # contains the link to the Rietveld issue, while the Rietveld message contains
4364 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004365 # Keep a separate copy for the commit message.
4366 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004367 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004368
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004369 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004370 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004371 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004372 # after it. Add a period on a new line to circumvent this. Also add a space
4373 # before the period to make sure that Gitiles continues to correctly resolve
4374 # the URL.
4375 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004377 commit_desc.append_footer('Patch from %s.' % options.contributor)
4378
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004379 print('Description:')
4380 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004382 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004383 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004384 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004386 # We want to squash all this branch's commits into one commit with the proper
4387 # description. We do this by doing a "reset --soft" to the base branch (which
4388 # keeps the working copy the same), then dcommitting that. If origin/master
4389 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4390 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004392 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4393 # Delete the branches if they exist.
4394 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4395 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4396 result = RunGitWithCode(showref_cmd)
4397 if result[0] == 0:
4398 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399
4400 # We might be in a directory that's present in this branch but not in the
4401 # trunk. Move up to the top of the tree so that git commands that expect a
4402 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004403 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 if rel_base_path:
4405 os.chdir(rel_base_path)
4406
4407 # Stuff our change into the merge branch.
4408 # We wrap in a try...finally block so if anything goes wrong,
4409 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004410 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004411 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004412 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004413 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004414 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004415 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004416 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004417 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004418 RunGit(
4419 [
4420 'commit', '--author', options.contributor,
4421 '-m', commit_desc.description,
4422 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004424 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004425 if base_has_submodules:
4426 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4427 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4428 RunGit(['checkout', CHERRY_PICK_BRANCH])
4429 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004430 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004431 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004432 mirror = settings.GetGitMirror(remote)
4433 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004434 pending_prefix = settings.GetPendingRefPrefix()
4435 if not pending_prefix or branch.startswith(pending_prefix):
4436 # If not using refs/pending/heads/* at all, or target ref is already set
4437 # to pending, then push to the target ref directly.
4438 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004439 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004440 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004441 else:
4442 # Cherry-pick the change on top of pending ref and then push it.
4443 assert branch.startswith('refs/'), branch
4444 assert pending_prefix[-1] == '/', pending_prefix
4445 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004446 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004447 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004448 if retcode == 0:
4449 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450 else:
4451 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004452 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004453 'svn', 'dcommit',
4454 '-C%s' % options.similarity,
4455 '--no-rebase', '--rmdir',
4456 ]
4457 if settings.GetForceHttpsCommitUrl():
4458 # Allow forcing https commit URLs for some projects that don't allow
4459 # committing to http URLs (like Google Code).
4460 remote_url = cl.GetGitSvnRemoteUrl()
4461 if urlparse.urlparse(remote_url).scheme == 'http':
4462 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004463 cmd_args.append('--commit-url=%s' % remote_url)
4464 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004465 if 'Committed r' in output:
4466 revision = re.match(
4467 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4468 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004469 finally:
4470 # And then swap back to the original branch and clean up.
4471 RunGit(['checkout', '-q', cl.GetBranch()])
4472 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004473 if base_has_submodules:
4474 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004476 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004477 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004478 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004479
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004480 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004481 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004482 try:
4483 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4484 # We set pushed_to_pending to False, since it made it all the way to the
4485 # real ref.
4486 pushed_to_pending = False
4487 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004488 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004489
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004491 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004492 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004493 if not to_pending:
4494 if viewvc_url and revision:
4495 change_desc.append_footer(
4496 'Committed: %s%s' % (viewvc_url, revision))
4497 elif revision:
4498 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('Closing issue '
4500 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004501 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004502 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004503 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004504 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004505 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004506 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004507 if options.bypass_hooks:
4508 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4509 else:
4510 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004511 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004512
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004513 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004514 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004515 print('The commit is in the pending queue (%s).' % pending_ref)
4516 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4517 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004518
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004519 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4520 if os.path.isfile(hook):
4521 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004522
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004523 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004524
4525
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004526def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004527 print()
4528 print('Waiting for commit to be landed on %s...' % real_ref)
4529 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004530 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4531 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004532 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004533
4534 loop = 0
4535 while True:
4536 sys.stdout.write('fetching (%d)... \r' % loop)
4537 sys.stdout.flush()
4538 loop += 1
4539
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004540 if mirror:
4541 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004542 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4543 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4544 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4545 for commit in commits.splitlines():
4546 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004547 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004548 return commit
4549
4550 current_rev = to_rev
4551
4552
tandriibf429402016-09-14 07:09:12 -07004553def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004554 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4555
4556 Returns:
4557 (retcode of last operation, output log of last operation).
4558 """
4559 assert pending_ref.startswith('refs/'), pending_ref
4560 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4561 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4562 code = 0
4563 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004564 max_attempts = 3
4565 attempts_left = max_attempts
4566 while attempts_left:
4567 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004568 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004569 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004570
4571 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004572 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004573 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004574 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004575 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004577 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004578 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004579 continue
4580
4581 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004582 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004583 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004584 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004585 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004586 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4587 'the following files have merge conflicts:' % pending_ref)
4588 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4589 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004590 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004591 return code, out
4592
4593 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004594 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004595 code, out = RunGitWithCode(
4596 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4597 if code == 0:
4598 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004599 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004600 return code, out
4601
vapiera7fbd5a2016-06-16 09:17:49 -07004602 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004603 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004604 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004605 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004606 print('Fatal push error. Make sure your .netrc credentials and git '
4607 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004608 return code, out
4609
vapiera7fbd5a2016-06-16 09:17:49 -07004610 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004611 return code, out
4612
4613
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004614def IsFatalPushFailure(push_stdout):
4615 """True if retrying push won't help."""
4616 return '(prohibited by Gerrit)' in push_stdout
4617
4618
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004619@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004620def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004621 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004622 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004623 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004624 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004625 message = """This repository appears to be a git-svn mirror, but we
4626don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004627 else:
4628 message = """This doesn't appear to be an SVN repository.
4629If your project has a true, writeable git repository, you probably want to run
4630'git cl land' instead.
4631If your project has a git mirror of an upstream SVN master, you probably need
4632to run 'git svn init'.
4633
4634Using the wrong command might cause your commit to appear to succeed, and the
4635review to be closed, without actually landing upstream. If you choose to
4636proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004637 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004638 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004639 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4640 'Please let us know of this project you are committing to:'
4641 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642 return SendUpstream(parser, args, 'dcommit')
4643
4644
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004645@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004646def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004647 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004648 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649 print('This appears to be an SVN repository.')
4650 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004651 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004652 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004653 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654
4655
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004656@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004657def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004658 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659 parser.add_option('-b', dest='newbranch',
4660 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004661 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004663 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4664 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004665 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004666 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004667 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004668 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004669 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004670 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004671
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004672
4673 group = optparse.OptionGroup(
4674 parser,
4675 'Options for continuing work on the current issue uploaded from a '
4676 'different clone (e.g. different machine). Must be used independently '
4677 'from the other options. No issue number should be specified, and the '
4678 'branch must have an issue number associated with it')
4679 group.add_option('--reapply', action='store_true', dest='reapply',
4680 help='Reset the branch and reapply the issue.\n'
4681 'CAUTION: This will undo any local changes in this '
4682 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004683
4684 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004685 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004686 parser.add_option_group(group)
4687
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004688 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004689 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004691 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004692 auth_config = auth.extract_auth_config_from_options(options)
4693
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004694
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004695 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004696 if options.newbranch:
4697 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004698 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004699 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004700
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004701 cl = Changelist(auth_config=auth_config,
4702 codereview=options.forced_codereview)
4703 if not cl.GetIssue():
4704 parser.error('current branch must have an associated issue')
4705
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004706 upstream = cl.GetUpstreamBranch()
4707 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004708 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004709
4710 RunGit(['reset', '--hard', upstream])
4711 if options.pull:
4712 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004713
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004714 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4715 options.directory)
4716
4717 if len(args) != 1 or not args[0]:
4718 parser.error('Must specify issue number or url')
4719
4720 # We don't want uncommitted changes mixed up with the patch.
4721 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004722 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004723
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004724 if options.newbranch:
4725 if options.force:
4726 RunGit(['branch', '-D', options.newbranch],
4727 stderr=subprocess2.PIPE, error_ok=True)
4728 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004729 elif not GetCurrentBranch():
4730 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004731
4732 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4733
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004734 if cl.IsGerrit():
4735 if options.reject:
4736 parser.error('--reject is not supported with Gerrit codereview.')
4737 if options.nocommit:
4738 parser.error('--nocommit is not supported with Gerrit codereview.')
4739 if options.directory:
4740 parser.error('--directory is not supported with Gerrit codereview.')
4741
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004742 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004743 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004744
4745
4746def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004747 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004748 # Provide a wrapper for git svn rebase to help avoid accidental
4749 # git svn dcommit.
4750 # It's the only command that doesn't use parser at all since we just defer
4751 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004752
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004753 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004754
4755
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004756def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004757 """Fetches the tree status and returns either 'open', 'closed',
4758 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004759 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760 if url:
4761 status = urllib2.urlopen(url).read().lower()
4762 if status.find('closed') != -1 or status == '0':
4763 return 'closed'
4764 elif status.find('open') != -1 or status == '1':
4765 return 'open'
4766 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004767 return 'unset'
4768
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004769
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004770def GetTreeStatusReason():
4771 """Fetches the tree status from a json url and returns the message
4772 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004773 url = settings.GetTreeStatusUrl()
4774 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004775 connection = urllib2.urlopen(json_url)
4776 status = json.loads(connection.read())
4777 connection.close()
4778 return status['message']
4779
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004781def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004782 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004783 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004784 status = GetTreeStatus()
4785 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004786 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004787 return 2
4788
vapiera7fbd5a2016-06-16 09:17:49 -07004789 print('The tree is %s' % status)
4790 print()
4791 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004792 if status != 'open':
4793 return 1
4794 return 0
4795
4796
maruel@chromium.org15192402012-09-06 12:38:29 +00004797def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004798 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004799 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004800 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004801 '-b', '--bot', action='append',
4802 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4803 'times to specify multiple builders. ex: '
4804 '"-b win_rel -b win_layout". See '
4805 'the try server waterfall for the builders name and the tests '
4806 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004807 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004808 '-B', '--bucket', default='',
4809 help=('Buildbucket bucket to send the try requests.'))
4810 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004811 '-m', '--master', default='',
4812 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004813 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004814 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004815 help='Revision to use for the try job; default: the revision will '
4816 'be determined by the try recipe that builder runs, which usually '
4817 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004818 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004819 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004820 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004821 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004822 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004823 '--project',
4824 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004825 'in recipe to determine to which repository or directory to '
4826 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004827 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004828 '-p', '--property', dest='properties', action='append', default=[],
4829 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004830 'key2=value2 etc. The value will be treated as '
4831 'json if decodable, or as string otherwise. '
4832 'NOTE: using this may make your try job not usable for CQ, '
4833 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004834 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004835 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4836 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004837 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004838 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004839 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004840 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004841
machenbach@chromium.org45453142015-09-15 08:45:22 +00004842 # Make sure that all properties are prop=value pairs.
4843 bad_params = [x for x in options.properties if '=' not in x]
4844 if bad_params:
4845 parser.error('Got properties with missing "=": %s' % bad_params)
4846
maruel@chromium.org15192402012-09-06 12:38:29 +00004847 if args:
4848 parser.error('Unknown arguments: %s' % args)
4849
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004850 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004851 if not cl.GetIssue():
4852 parser.error('Need to upload first')
4853
tandriie113dfd2016-10-11 10:20:12 -07004854 error_message = cl.CannotTriggerTryJobReason()
4855 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004856 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004857
borenet6c0efe62016-10-19 08:13:29 -07004858 if options.bucket and options.master:
4859 parser.error('Only one of --bucket and --master may be used.')
4860
qyearsley1fdfcb62016-10-24 13:22:03 -07004861 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004862
qyearsleydd49f942016-10-28 11:57:22 -07004863 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4864 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004865 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004866 if options.verbose:
4867 print('git cl try with no bots now defaults to CQ Dry Run.')
4868 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004869
borenet6c0efe62016-10-19 08:13:29 -07004870 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004871 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004872 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004873 'of bot requires an initial job from a parent (usually a builder). '
4874 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004875 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004876 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004877
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004878 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004879 if patchset != cl.GetPatchset():
4880 print('Warning: Codereview server has newer patchsets (%s) than most '
4881 'recent upload from local checkout (%s). Did a previous upload '
4882 'fail?\n'
4883 'By default, git cl try uses the latest patchset from '
4884 'codereview, continuing to use patchset %s.\n' %
4885 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004886
tandrii568043b2016-10-11 07:49:18 -07004887 try:
borenet6c0efe62016-10-19 08:13:29 -07004888 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4889 patchset)
tandrii568043b2016-10-11 07:49:18 -07004890 except BuildbucketResponseException as ex:
4891 print('ERROR: %s' % ex)
4892 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004893 return 0
4894
4895
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004896def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004897 """Prints info about try jobs associated with current CL."""
4898 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004899 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004900 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004901 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004902 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004903 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004904 '--color', action='store_true', default=setup_color.IS_TTY,
4905 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004906 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004907 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4908 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004909 group.add_option(
4910 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004911 parser.add_option_group(group)
4912 auth.add_auth_options(parser)
4913 options, args = parser.parse_args(args)
4914 if args:
4915 parser.error('Unrecognized args: %s' % ' '.join(args))
4916
4917 auth_config = auth.extract_auth_config_from_options(options)
4918 cl = Changelist(auth_config=auth_config)
4919 if not cl.GetIssue():
4920 parser.error('Need to upload first')
4921
tandrii221ab252016-10-06 08:12:04 -07004922 patchset = options.patchset
4923 if not patchset:
4924 patchset = cl.GetMostRecentPatchset()
4925 if not patchset:
4926 parser.error('Codereview doesn\'t know about issue %s. '
4927 'No access to issue or wrong issue number?\n'
4928 'Either upload first, or pass --patchset explicitely' %
4929 cl.GetIssue())
4930
4931 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004932 print('Warning: Codereview server has newer patchsets (%s) than most '
4933 'recent upload from local checkout (%s). Did a previous upload '
4934 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004935 'By default, git cl try-results uses the latest patchset from '
4936 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004937 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004938 try:
tandrii221ab252016-10-06 08:12:04 -07004939 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004940 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004941 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004942 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004943 if options.json:
4944 write_try_results_json(options.json, jobs)
4945 else:
4946 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004947 return 0
4948
4949
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004950@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004951def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004952 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004953 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004954 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004955 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004957 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004958 if args:
4959 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004960 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004961 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004962 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004963 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004964
4965 # Clear configured merge-base, if there is one.
4966 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004967 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004968 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004969 return 0
4970
4971
thestig@chromium.org00858c82013-12-02 23:08:03 +00004972def CMDweb(parser, args):
4973 """Opens the current CL in the web browser."""
4974 _, args = parser.parse_args(args)
4975 if args:
4976 parser.error('Unrecognized args: %s' % ' '.join(args))
4977
4978 issue_url = Changelist().GetIssueURL()
4979 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004980 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004981 return 1
4982
4983 webbrowser.open(issue_url)
4984 return 0
4985
4986
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004987def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004988 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004989 parser.add_option('-d', '--dry-run', action='store_true',
4990 help='trigger in dry run mode')
4991 parser.add_option('-c', '--clear', action='store_true',
4992 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004993 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004994 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004995 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004996 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004997 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004998 if args:
4999 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005000 if options.dry_run and options.clear:
5001 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5002
iannuccie53c9352016-08-17 14:40:40 -07005003 cl = Changelist(auth_config=auth_config, issue=options.issue,
5004 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005005 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005006 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005007 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005008 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005009 state = _CQState.DRY_RUN
5010 else:
5011 state = _CQState.COMMIT
5012 if not cl.GetIssue():
5013 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005014 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005015 return 0
5016
5017
groby@chromium.org411034a2013-02-26 15:12:01 +00005018def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005019 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005020 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005021 auth.add_auth_options(parser)
5022 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005023 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005024 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005025 if args:
5026 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005027 cl = Changelist(auth_config=auth_config, issue=options.issue,
5028 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005029 # Ensure there actually is an issue to close.
5030 cl.GetDescription()
5031 cl.CloseIssue()
5032 return 0
5033
5034
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005035def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005036 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005037 parser.add_option(
5038 '--stat',
5039 action='store_true',
5040 dest='stat',
5041 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005042 auth.add_auth_options(parser)
5043 options, args = parser.parse_args(args)
5044 auth_config = auth.extract_auth_config_from_options(options)
5045 if args:
5046 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005047
5048 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005049 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005050 # Staged changes would be committed along with the patch from last
5051 # upload, hence counted toward the "last upload" side in the final
5052 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005053 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005054 return 1
5055
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005056 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005057 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005058 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005059 if not issue:
5060 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005061 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005062 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005063
5064 # Create a new branch based on the merge-base
5065 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005066 # Clear cached branch in cl object, to avoid overwriting original CL branch
5067 # properties.
5068 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005069 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005070 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005071 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005072 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005073 return rtn
5074
wychen@chromium.org06928532015-02-03 02:11:29 +00005075 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005076 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005077 cmd = ['git', 'diff']
5078 if options.stat:
5079 cmd.append('--stat')
5080 cmd.extend([TMP_BRANCH, branch, '--'])
5081 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005082 finally:
5083 RunGit(['checkout', '-q', branch])
5084 RunGit(['branch', '-D', TMP_BRANCH])
5085
5086 return 0
5087
5088
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005089def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005090 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005091 parser.add_option(
5092 '--no-color',
5093 action='store_true',
5094 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005095 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005096 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005097 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005098
5099 author = RunGit(['config', 'user.email']).strip() or None
5100
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005101 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005102
5103 if args:
5104 if len(args) > 1:
5105 parser.error('Unknown args')
5106 base_branch = args[0]
5107 else:
5108 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005109 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005110
5111 change = cl.GetChange(base_branch, None)
5112 return owners_finder.OwnersFinder(
5113 [f.LocalPath() for f in
5114 cl.GetChange(base_branch, None).AffectedFiles()],
5115 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005116 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005117 disable_color=options.no_color).run()
5118
5119
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005120def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005121 """Generates a diff command."""
5122 # Generate diff for the current branch's changes.
5123 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5124 upstream_commit, '--' ]
5125
5126 if args:
5127 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005129 diff_cmd.append(arg)
5130 else:
5131 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005132
5133 return diff_cmd
5134
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005135def MatchingFileType(file_name, extensions):
5136 """Returns true if the file name ends with one of the given extensions."""
5137 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005138
enne@chromium.org555cfe42014-01-29 18:21:39 +00005139@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005140def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005141 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005142 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005143 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005144 parser.add_option('--full', action='store_true',
5145 help='Reformat the full content of all touched files')
5146 parser.add_option('--dry-run', action='store_true',
5147 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005148 parser.add_option('--python', action='store_true',
5149 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005150 parser.add_option('--diff', action='store_true',
5151 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005152 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005153
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005154 # git diff generates paths against the root of the repository. Change
5155 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005156 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005157 if rel_base_path:
5158 os.chdir(rel_base_path)
5159
digit@chromium.org29e47272013-05-17 17:01:46 +00005160 # Grab the merge-base commit, i.e. the upstream commit of the current
5161 # branch when it was created or the last time it was rebased. This is
5162 # to cover the case where the user may have called "git fetch origin",
5163 # moving the origin branch to a newer commit, but hasn't rebased yet.
5164 upstream_commit = None
5165 cl = Changelist()
5166 upstream_branch = cl.GetUpstreamBranch()
5167 if upstream_branch:
5168 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5169 upstream_commit = upstream_commit.strip()
5170
5171 if not upstream_commit:
5172 DieWithError('Could not find base commit for this branch. '
5173 'Are you in detached state?')
5174
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005175 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5176 diff_output = RunGit(changed_files_cmd)
5177 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005178 # Filter out files deleted by this CL
5179 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005180
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005181 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5182 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5183 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005184 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005185
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005186 top_dir = os.path.normpath(
5187 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5188
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005189 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5190 # formatted. This is used to block during the presubmit.
5191 return_value = 0
5192
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005193 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005194 # Locate the clang-format binary in the checkout
5195 try:
5196 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005197 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005198 DieWithError(e)
5199
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005200 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005201 cmd = [clang_format_tool]
5202 if not opts.dry_run and not opts.diff:
5203 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005204 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005205 if opts.diff:
5206 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005207 else:
5208 env = os.environ.copy()
5209 env['PATH'] = str(os.path.dirname(clang_format_tool))
5210 try:
5211 script = clang_format.FindClangFormatScriptInChromiumTree(
5212 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005213 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005214 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005215
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005216 cmd = [sys.executable, script, '-p0']
5217 if not opts.dry_run and not opts.diff:
5218 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005219
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005220 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5221 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005222
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005223 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5224 if opts.diff:
5225 sys.stdout.write(stdout)
5226 if opts.dry_run and len(stdout) > 0:
5227 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005228
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005229 # Similar code to above, but using yapf on .py files rather than clang-format
5230 # on C/C++ files
5231 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005232 yapf_tool = gclient_utils.FindExecutable('yapf')
5233 if yapf_tool is None:
5234 DieWithError('yapf not found in PATH')
5235
5236 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005237 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005238 cmd = [yapf_tool]
5239 if not opts.dry_run and not opts.diff:
5240 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005241 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005242 if opts.diff:
5243 sys.stdout.write(stdout)
5244 else:
5245 # TODO(sbc): yapf --lines mode still has some issues.
5246 # https://github.com/google/yapf/issues/154
5247 DieWithError('--python currently only works with --full')
5248
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005249 # Dart's formatter does not have the nice property of only operating on
5250 # modified chunks, so hard code full.
5251 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005252 try:
5253 command = [dart_format.FindDartFmtToolInChromiumTree()]
5254 if not opts.dry_run and not opts.diff:
5255 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005256 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005257
ppi@chromium.org6593d932016-03-03 15:41:15 +00005258 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005259 if opts.dry_run and stdout:
5260 return_value = 2
5261 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005262 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5263 'found in this checkout. Files in other languages are still '
5264 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005265
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005266 # Format GN build files. Always run on full build files for canonical form.
5267 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005268 cmd = ['gn', 'format' ]
5269 if opts.dry_run or opts.diff:
5270 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005271 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005272 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5273 shell=sys.platform == 'win32',
5274 cwd=top_dir)
5275 if opts.dry_run and gn_ret == 2:
5276 return_value = 2 # Not formatted.
5277 elif opts.diff and gn_ret == 2:
5278 # TODO this should compute and print the actual diff.
5279 print("This change has GN build file diff for " + gn_diff_file)
5280 elif gn_ret != 0:
5281 # For non-dry run cases (and non-2 return values for dry-run), a
5282 # nonzero error code indicates a failure, probably because the file
5283 # doesn't parse.
5284 DieWithError("gn format failed on " + gn_diff_file +
5285 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005286
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005287 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005288
5289
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005290@subcommand.usage('<codereview url or issue id>')
5291def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005292 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005293 _, args = parser.parse_args(args)
5294
5295 if len(args) != 1:
5296 parser.print_help()
5297 return 1
5298
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005299 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005300 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005301 parser.print_help()
5302 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005303 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005304
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005305 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005306 output = RunGit(['config', '--local', '--get-regexp',
5307 r'branch\..*\.%s' % issueprefix],
5308 error_ok=True)
5309 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005310 if issue == target_issue:
5311 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005312
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005313 branches = []
5314 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005315 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005316 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005317 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005318 return 1
5319 if len(branches) == 1:
5320 RunGit(['checkout', branches[0]])
5321 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005322 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005323 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005324 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005325 which = raw_input('Choose by index: ')
5326 try:
5327 RunGit(['checkout', branches[int(which)]])
5328 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005329 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005330 return 1
5331
5332 return 0
5333
5334
maruel@chromium.org29404b52014-09-08 22:58:00 +00005335def CMDlol(parser, args):
5336 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005337 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005338 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5339 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5340 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005341 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005342 return 0
5343
5344
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005345class OptionParser(optparse.OptionParser):
5346 """Creates the option parse and add --verbose support."""
5347 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005348 optparse.OptionParser.__init__(
5349 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005350 self.add_option(
5351 '-v', '--verbose', action='count', default=0,
5352 help='Use 2 times for more debugging info')
5353
5354 def parse_args(self, args=None, values=None):
5355 options, args = optparse.OptionParser.parse_args(self, args, values)
5356 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5357 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5358 return options, args
5359
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005360
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005361def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005362 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005363 print('\nYour python version %s is unsupported, please upgrade.\n' %
5364 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005365 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005366
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005367 # Reload settings.
5368 global settings
5369 settings = Settings()
5370
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005371 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005372 dispatcher = subcommand.CommandDispatcher(__name__)
5373 try:
5374 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005375 except auth.AuthenticationError as e:
5376 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005377 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005378 if e.code != 500:
5379 raise
5380 DieWithError(
5381 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5382 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005383 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005384
5385
5386if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005387 # These affect sys.stdout so do it outside of main() to simplify mocks in
5388 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005389 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005390 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005391 try:
5392 sys.exit(main(sys.argv[1:]))
5393 except KeyboardInterrupt:
5394 sys.stderr.write('interrupted\n')
5395 sys.exit(1)