blob: 5d4a6b96eb36f36b21bc2ba8c4b9341f4b86ba32 [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.
nodire4f0fe02016-11-04 16:23:30 -0700353 masters = 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)
nodire4f0fe02016-11-04 16:23:30 -0700361 if masters is None:
362 return None
363 return {MASTER_PREFIX + m: b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700364
qyearsley1fdfcb62016-10-24 13:22:03 -0700365 if options.bucket:
366 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700367 if options.master:
368 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700369
qyearsleydd49f942016-10-28 11:57:22 -0700370 # If bots are listed but no master or bucket, then we need to find out
371 # the corresponding master for each bot.
372 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
373 if error_message:
374 option_parser.error(
375 'Tryserver master cannot be found because: %s\n'
376 'Please manually specify the tryserver master, e.g. '
377 '"-m tryserver.chromium.linux".' % error_message)
378 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700379
380
qyearsley123a4682016-10-26 09:12:17 -0700381def _get_bucket_map_for_builders(builders):
382 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 map_url = 'https://builders-map.appspot.com/'
384 try:
qyearsley123a4682016-10-26 09:12:17 -0700385 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 except urllib2.URLError as e:
387 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
388 (map_url, e))
389 except ValueError as e:
390 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700391 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 return None, 'Failed to build master map.'
393
qyearsley123a4682016-10-26 09:12:17 -0700394 bucket_map = {}
395 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700396 masters = builders_map.get(builder, [])
397 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700398 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700399 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700401 (builder, masters))
402 bucket = _prefix_master(masters[0])
403 bucket_map.setdefault(bucket, {})[builder] = []
404
405 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
borenet6c0efe62016-10-19 08:13:29 -0700408def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700409 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 """Sends a request to Buildbucket to trigger try jobs for a changelist.
411
412 Args:
413 auth_config: AuthConfig for Rietveld.
414 changelist: Changelist that the try jobs are associated with.
415 buckets: A nested dict mapping bucket names to builders to tests.
416 options: Command-line options.
417 """
tandriide281ae2016-10-12 06:02:30 -0700418 assert changelist.GetIssue(), 'CL must be uploaded first'
419 codereview_url = changelist.GetCodereviewServer()
420 assert codereview_url, 'CL must be uploaded first'
421 patchset = patchset or changelist.GetMostRecentPatchset()
422 assert patchset, 'CL must be uploaded first'
423
424 codereview_host = urlparse.urlparse(codereview_url).hostname
425 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000426 http = authenticator.authorize(httplib2.Http())
427 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700428
429 # TODO(tandrii): consider caching Gerrit CL details just like
430 # _RietveldChangelistImpl does, then caching values in these two variables
431 # won't be necessary.
432 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000433
434 buildbucket_put_url = (
435 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000436 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700437 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
438 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
439 hostname=codereview_host,
440 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000441 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700442
443 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
444 shared_parameters_properties['category'] = category
445 if options.clobber:
446 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700447 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700448 if extra_properties:
449 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000450
451 batch_req_body = {'builds': []}
452 print_text = []
453 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700454 for bucket, builders_and_tests in sorted(buckets.iteritems()):
455 print_text.append('Bucket: %s' % bucket)
456 master = None
457 if bucket.startswith(MASTER_PREFIX):
458 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 for builder, tests in sorted(builders_and_tests.iteritems()):
460 print_text.append(' %s: %s' % (builder, tests))
461 parameters = {
462 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000463 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700464 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000465 'revision': options.revision,
466 }],
tandrii8c5a3532016-11-04 07:52:02 -0700467 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000469 if 'presubmit' in builder.lower():
470 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000471 if tests:
472 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700473
474 tags = [
475 'builder:%s' % builder,
476 'buildset:%s' % buildset,
477 'user_agent:git_cl_try',
478 ]
479 if master:
480 parameters['properties']['master'] = master
481 tags.append('master:%s' % master)
482
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 batch_req_body['builds'].append(
484 {
485 'bucket': bucket,
486 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000487 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700488 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 }
490 )
491
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000492 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700493 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 http,
495 buildbucket_put_url,
496 'PUT',
497 body=json.dumps(batch_req_body),
498 headers={'Content-Type': 'application/json'}
499 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000500 print_text.append('To see results here, run: git cl try-results')
501 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700502 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000503
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000504
tandrii221ab252016-10-06 08:12:04 -0700505def fetch_try_jobs(auth_config, changelist, buildbucket_host,
506 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700507 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000508
qyearsley53f48a12016-09-01 10:45:13 -0700509 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 """
tandrii221ab252016-10-06 08:12:04 -0700511 assert buildbucket_host
512 assert changelist.GetIssue(), 'CL must be uploaded first'
513 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
514 patchset = patchset or changelist.GetMostRecentPatchset()
515 assert patchset, 'CL must be uploaded first'
516
517 codereview_url = changelist.GetCodereviewServer()
518 codereview_host = urlparse.urlparse(codereview_url).hostname
519 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 if authenticator.has_cached_credentials():
521 http = authenticator.authorize(httplib2.Http())
522 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700523 print('Warning: Some results might be missing because %s' %
524 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700525 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 http = httplib2.Http()
527
528 http.force_exception_to_status_code = True
529
tandrii221ab252016-10-06 08:12:04 -0700530 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
531 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
532 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700534 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 params = {'tag': 'buildset:%s' % buildset}
536
537 builds = {}
538 while True:
539 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700540 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700542 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 for build in content.get('builds', []):
544 builds[build['id']] = build
545 if 'next_cursor' in content:
546 params['start_cursor'] = content['next_cursor']
547 else:
548 break
549 return builds
550
551
qyearsleyeab3c042016-08-24 09:18:28 -0700552def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 """Prints nicely result of fetch_try_jobs."""
554 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700555 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 return
557
558 # Make a copy, because we'll be modifying builds dictionary.
559 builds = builds.copy()
560 builder_names_cache = {}
561
562 def get_builder(b):
563 try:
564 return builder_names_cache[b['id']]
565 except KeyError:
566 try:
567 parameters = json.loads(b['parameters_json'])
568 name = parameters['builder_name']
569 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700570 print('WARNING: failed to get builder name for build %s: %s' % (
571 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 name = None
573 builder_names_cache[b['id']] = name
574 return name
575
576 def get_bucket(b):
577 bucket = b['bucket']
578 if bucket.startswith('master.'):
579 return bucket[len('master.'):]
580 return bucket
581
582 if options.print_master:
583 name_fmt = '%%-%ds %%-%ds' % (
584 max(len(str(get_bucket(b))) for b in builds.itervalues()),
585 max(len(str(get_builder(b))) for b in builds.itervalues()))
586 def get_name(b):
587 return name_fmt % (get_bucket(b), get_builder(b))
588 else:
589 name_fmt = '%%-%ds' % (
590 max(len(str(get_builder(b))) for b in builds.itervalues()))
591 def get_name(b):
592 return name_fmt % get_builder(b)
593
594 def sort_key(b):
595 return b['status'], b.get('result'), get_name(b), b.get('url')
596
597 def pop(title, f, color=None, **kwargs):
598 """Pop matching builds from `builds` dict and print them."""
599
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000600 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 colorize = str
602 else:
603 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
604
605 result = []
606 for b in builds.values():
607 if all(b.get(k) == v for k, v in kwargs.iteritems()):
608 builds.pop(b['id'])
609 result.append(b)
610 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700611 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700613 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614
615 total = len(builds)
616 pop(status='COMPLETED', result='SUCCESS',
617 title='Successes:', color=Fore.GREEN,
618 f=lambda b: (get_name(b), b.get('url')))
619 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
620 title='Infra Failures:', color=Fore.MAGENTA,
621 f=lambda b: (get_name(b), b.get('url')))
622 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
623 title='Failures:', color=Fore.RED,
624 f=lambda b: (get_name(b), b.get('url')))
625 pop(status='COMPLETED', result='CANCELED',
626 title='Canceled:', color=Fore.MAGENTA,
627 f=lambda b: (get_name(b),))
628 pop(status='COMPLETED', result='FAILURE',
629 failure_reason='INVALID_BUILD_DEFINITION',
630 title='Wrong master/builder name:', color=Fore.MAGENTA,
631 f=lambda b: (get_name(b),))
632 pop(status='COMPLETED', result='FAILURE',
633 title='Other failures:',
634 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
635 pop(status='COMPLETED',
636 title='Other finished:',
637 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
638 pop(status='STARTED',
639 title='Started:', color=Fore.YELLOW,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='SCHEDULED',
642 title='Scheduled:',
643 f=lambda b: (get_name(b), 'id=%s' % b['id']))
644 # The last section is just in case buildbucket API changes OR there is a bug.
645 pop(title='Other:',
646 f=lambda b: (get_name(b), 'id=%s' % b['id']))
647 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700648 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649
650
qyearsley53f48a12016-09-01 10:45:13 -0700651def write_try_results_json(output_file, builds):
652 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
653
654 The input |builds| dict is assumed to be generated by Buildbucket.
655 Buildbucket documentation: http://goo.gl/G0s101
656 """
657
658 def convert_build_dict(build):
659 return {
660 'buildbucket_id': build.get('id'),
661 'status': build.get('status'),
662 'result': build.get('result'),
663 'bucket': build.get('bucket'),
664 'builder_name': json.loads(
665 build.get('parameters_json', '{}')).get('builder_name'),
666 'failure_reason': build.get('failure_reason'),
667 'url': build.get('url'),
668 }
669
670 converted = []
671 for _, build in sorted(builds.items()):
672 converted.append(convert_build_dict(build))
673 write_json(output_file, converted)
674
675
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000676def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
677 """Return the corresponding git ref if |base_url| together with |glob_spec|
678 matches the full |url|.
679
680 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
681 """
682 fetch_suburl, as_ref = glob_spec.split(':')
683 if allow_wildcards:
684 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
685 if glob_match:
686 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
687 # "branches/{472,597,648}/src:refs/remotes/svn/*".
688 branch_re = re.escape(base_url)
689 if glob_match.group(1):
690 branch_re += '/' + re.escape(glob_match.group(1))
691 wildcard = glob_match.group(2)
692 if wildcard == '*':
693 branch_re += '([^/]*)'
694 else:
695 # Escape and replace surrounding braces with parentheses and commas
696 # with pipe symbols.
697 wildcard = re.escape(wildcard)
698 wildcard = re.sub('^\\\\{', '(', wildcard)
699 wildcard = re.sub('\\\\,', '|', wildcard)
700 wildcard = re.sub('\\\\}$', ')', wildcard)
701 branch_re += wildcard
702 if glob_match.group(3):
703 branch_re += re.escape(glob_match.group(3))
704 match = re.match(branch_re, url)
705 if match:
706 return re.sub('\*$', match.group(1), as_ref)
707
708 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
709 if fetch_suburl:
710 full_url = base_url + '/' + fetch_suburl
711 else:
712 full_url = base_url
713 if full_url == url:
714 return as_ref
715 return None
716
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000717
iannucci@chromium.org79540052012-10-19 23:15:26 +0000718def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000719 """Prints statistics about the change to the user."""
720 # --no-ext-diff is broken in some versions of Git, so try to work around
721 # this by overriding the environment (but there is still a problem if the
722 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000724 if 'GIT_EXTERNAL_DIFF' in env:
725 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000726
727 if find_copies:
728 similarity_options = ['--find-copies-harder', '-l100000',
729 '-C%s' % similarity]
730 else:
731 similarity_options = ['-M%s' % similarity]
732
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000733 try:
734 stdout = sys.stdout.fileno()
735 except AttributeError:
736 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000737 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000738 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000739 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000740 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000741
742
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000743class BuildbucketResponseException(Exception):
744 pass
745
746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747class Settings(object):
748 def __init__(self):
749 self.default_server = None
750 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000751 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 self.is_git_svn = None
753 self.svn_branch = None
754 self.tree_status_url = None
755 self.viewvc_url = None
756 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000757 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000758 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000759 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000760 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000761 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000762 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000763 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700764 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765
766 def LazyUpdateIfNeeded(self):
767 """Updates the settings from a codereview.settings file, if available."""
768 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000769 # The only value that actually changes the behavior is
770 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000771 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000772 error_ok=True
773 ).strip().lower()
774
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000776 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 LoadCodereviewSettingsFromFile(cr_settings_file)
778 self.updated = True
779
780 def GetDefaultServerUrl(self, error_ok=False):
781 if not self.default_server:
782 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000783 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000784 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 if error_ok:
786 return self.default_server
787 if not self.default_server:
788 error_message = ('Could not find settings file. You must configure '
789 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000790 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 return self.default_server
793
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000794 @staticmethod
795 def GetRelativeRoot():
796 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000799 if self.root is None:
800 self.root = os.path.abspath(self.GetRelativeRoot())
801 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000803 def GetGitMirror(self, remote='origin'):
804 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000805 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000806 if not os.path.isdir(local_url):
807 return None
808 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
809 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
810 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
811 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
812 if mirror.exists():
813 return mirror
814 return None
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816 def GetIsGitSvn(self):
817 """Return true if this repo looks like it's using git-svn."""
818 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000819 if self.GetPendingRefPrefix():
820 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
821 self.is_git_svn = False
822 else:
823 # If you have any "svn-remote.*" config keys, we think you're using svn.
824 self.is_git_svn = RunGitWithCode(
825 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 return self.is_git_svn
827
828 def GetSVNBranch(self):
829 if self.svn_branch is None:
830 if not self.GetIsGitSvn():
831 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
832
833 # Try to figure out which remote branch we're based on.
834 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000835 # 1) iterate through our branch history and find the svn URL.
836 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
838 # regexp matching the git-svn line that contains the URL.
839 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
840
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000841 # We don't want to go through all of history, so read a line from the
842 # pipe at a time.
843 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000844 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000845 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
846 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000847 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000848 for line in proc.stdout:
849 match = git_svn_re.match(line)
850 if match:
851 url = match.group(1)
852 proc.stdout.close() # Cut pipe.
853 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000855 if url:
856 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
857 remotes = RunGit(['config', '--get-regexp',
858 r'^svn-remote\..*\.url']).splitlines()
859 for remote in remotes:
860 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000862 remote = match.group(1)
863 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000864 rewrite_root = RunGit(
865 ['config', 'svn-remote.%s.rewriteRoot' % remote],
866 error_ok=True).strip()
867 if rewrite_root:
868 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000869 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000870 ['config', 'svn-remote.%s.fetch' % remote],
871 error_ok=True).strip()
872 if fetch_spec:
873 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
874 if self.svn_branch:
875 break
876 branch_spec = RunGit(
877 ['config', 'svn-remote.%s.branches' % remote],
878 error_ok=True).strip()
879 if branch_spec:
880 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
881 if self.svn_branch:
882 break
883 tag_spec = RunGit(
884 ['config', 'svn-remote.%s.tags' % remote],
885 error_ok=True).strip()
886 if tag_spec:
887 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
888 if self.svn_branch:
889 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890
891 if not self.svn_branch:
892 DieWithError('Can\'t guess svn branch -- try specifying it on the '
893 'command line')
894
895 return self.svn_branch
896
897 def GetTreeStatusUrl(self, error_ok=False):
898 if not self.tree_status_url:
899 error_message = ('You must configure your tree status URL by running '
900 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000901 self.tree_status_url = self._GetRietveldConfig(
902 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 return self.tree_status_url
904
905 def GetViewVCUrl(self):
906 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000907 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908 return self.viewvc_url
909
rmistry@google.com90752582014-01-14 21:04:50 +0000910 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000911 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000912
rmistry@google.com78948ed2015-07-08 23:09:57 +0000913 def GetIsSkipDependencyUpload(self, branch_name):
914 """Returns true if specified branch should skip dep uploads."""
915 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
916 error_ok=True)
917
rmistry@google.com5626a922015-02-26 14:03:30 +0000918 def GetRunPostUploadHook(self):
919 run_post_upload_hook = self._GetRietveldConfig(
920 'run-post-upload-hook', error_ok=True)
921 return run_post_upload_hook == "True"
922
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000923 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000924 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000925
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000926 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000927 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000928
ukai@chromium.orge8077812012-02-03 03:41:46 +0000929 def GetIsGerrit(self):
930 """Return true if this repo is assosiated with gerrit code review system."""
931 if self.is_gerrit is None:
932 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
933 return self.is_gerrit
934
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000935 def GetSquashGerritUploads(self):
936 """Return true if uploads to Gerrit should be squashed by default."""
937 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700938 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
939 if self.squash_gerrit_uploads is None:
940 # Default is squash now (http://crbug.com/611892#c23).
941 self.squash_gerrit_uploads = not (
942 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
943 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000944 return self.squash_gerrit_uploads
945
tandriia60502f2016-06-20 02:01:53 -0700946 def GetSquashGerritUploadsOverride(self):
947 """Return True or False if codereview.settings should be overridden.
948
949 Returns None if no override has been defined.
950 """
951 # See also http://crbug.com/611892#c23
952 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
953 error_ok=True).strip()
954 if result == 'true':
955 return True
956 if result == 'false':
957 return False
958 return None
959
tandrii@chromium.org28253532016-04-14 13:46:56 +0000960 def GetGerritSkipEnsureAuthenticated(self):
961 """Return True if EnsureAuthenticated should not be done for Gerrit
962 uploads."""
963 if self.gerrit_skip_ensure_authenticated is None:
964 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000965 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000966 error_ok=True).strip() == 'true')
967 return self.gerrit_skip_ensure_authenticated
968
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000969 def GetGitEditor(self):
970 """Return the editor specified in the git config, or None if none is."""
971 if self.git_editor is None:
972 self.git_editor = self._GetConfig('core.editor', error_ok=True)
973 return self.git_editor or None
974
thestig@chromium.org44202a22014-03-11 19:22:18 +0000975 def GetLintRegex(self):
976 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
977 DEFAULT_LINT_REGEX)
978
979 def GetLintIgnoreRegex(self):
980 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
981 DEFAULT_LINT_IGNORE_REGEX)
982
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000983 def GetProject(self):
984 if not self.project:
985 self.project = self._GetRietveldConfig('project', error_ok=True)
986 return self.project
987
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000988 def GetForceHttpsCommitUrl(self):
989 if not self.force_https_commit_url:
990 self.force_https_commit_url = self._GetRietveldConfig(
991 'force-https-commit-url', error_ok=True)
992 return self.force_https_commit_url
993
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000994 def GetPendingRefPrefix(self):
995 if not self.pending_ref_prefix:
996 self.pending_ref_prefix = self._GetRietveldConfig(
997 'pending-ref-prefix', error_ok=True)
998 return self.pending_ref_prefix
999
tandriif46c20f2016-09-14 06:17:05 -07001000 def GetHasGitNumberFooter(self):
1001 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1002 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1003 if not self.git_number_footer:
1004 self.git_number_footer = self._GetRietveldConfig(
1005 'git-number-footer', error_ok=True)
1006 return self.git_number_footer
1007
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001008 def _GetRietveldConfig(self, param, **kwargs):
1009 return self._GetConfig('rietveld.' + param, **kwargs)
1010
rmistry@google.com78948ed2015-07-08 23:09:57 +00001011 def _GetBranchConfig(self, branch_name, param, **kwargs):
1012 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1013
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 def _GetConfig(self, param, **kwargs):
1015 self.LazyUpdateIfNeeded()
1016 return RunGit(['config', param], **kwargs).strip()
1017
1018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019def ShortBranchName(branch):
1020 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001021 return branch.replace('refs/heads/', '', 1)
1022
1023
1024def GetCurrentBranchRef():
1025 """Returns branch ref (e.g., refs/heads/master) or None."""
1026 return RunGit(['symbolic-ref', 'HEAD'],
1027 stderr=subprocess2.VOID, error_ok=True).strip() or None
1028
1029
1030def GetCurrentBranch():
1031 """Returns current branch or None.
1032
1033 For refs/heads/* branches, returns just last part. For others, full ref.
1034 """
1035 branchref = GetCurrentBranchRef()
1036 if branchref:
1037 return ShortBranchName(branchref)
1038 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039
1040
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001041class _CQState(object):
1042 """Enum for states of CL with respect to Commit Queue."""
1043 NONE = 'none'
1044 DRY_RUN = 'dry_run'
1045 COMMIT = 'commit'
1046
1047 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1048
1049
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050class _ParsedIssueNumberArgument(object):
1051 def __init__(self, issue=None, patchset=None, hostname=None):
1052 self.issue = issue
1053 self.patchset = patchset
1054 self.hostname = hostname
1055
1056 @property
1057 def valid(self):
1058 return self.issue is not None
1059
1060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001061def ParseIssueNumberArgument(arg):
1062 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1063 fail_result = _ParsedIssueNumberArgument()
1064
1065 if arg.isdigit():
1066 return _ParsedIssueNumberArgument(issue=int(arg))
1067 if not arg.startswith('http'):
1068 return fail_result
1069 url = gclient_utils.UpgradeToHttps(arg)
1070 try:
1071 parsed_url = urlparse.urlparse(url)
1072 except ValueError:
1073 return fail_result
1074 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1075 tmp = cls.ParseIssueURL(parsed_url)
1076 if tmp is not None:
1077 return tmp
1078 return fail_result
1079
1080
tandriic2405f52016-10-10 08:13:15 -07001081class GerritIssueNotExists(Exception):
1082 def __init__(self, issue, url):
1083 self.issue = issue
1084 self.url = url
1085 super(GerritIssueNotExists, self).__init__()
1086
1087 def __str__(self):
1088 return 'issue %s at %s does not exist or you have no access to it' % (
1089 self.issue, self.url)
1090
1091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093 """Changelist works with one changelist in local branch.
1094
1095 Supports two codereview backends: Rietveld or Gerrit, selected at object
1096 creation.
1097
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001098 Notes:
1099 * Not safe for concurrent multi-{thread,process} use.
1100 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001101 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001102 """
1103
1104 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1105 """Create a new ChangeList instance.
1106
1107 If issue is given, the codereview must be given too.
1108
1109 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1110 Otherwise, it's decided based on current configuration of the local branch,
1111 with default being 'rietveld' for backwards compatibility.
1112 See _load_codereview_impl for more details.
1113
1114 **kwargs will be passed directly to codereview implementation.
1115 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001117 global settings
1118 if not settings:
1119 # Happens when git_cl.py is used as a utility library.
1120 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121
1122 if issue:
1123 assert codereview, 'codereview must be known, if issue is known'
1124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 self.branchref = branchref
1126 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001127 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.branch = ShortBranchName(self.branchref)
1129 else:
1130 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001132 self.lookedup_issue = False
1133 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.has_description = False
1135 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001138 self.cc = None
1139 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001140 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001141
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001143 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 assert self._codereview_impl
1146 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147
1148 def _load_codereview_impl(self, codereview=None, **kwargs):
1149 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001150 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1151 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1152 self._codereview = codereview
1153 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 return
1155
1156 # Automatic selection based on issue number set for a current branch.
1157 # Rietveld takes precedence over Gerrit.
1158 assert not self.issue
1159 # Whether we find issue or not, we are doing the lookup.
1160 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001161 if self.GetBranch():
1162 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1163 issue = _git_get_branch_config_value(
1164 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1165 if issue:
1166 self._codereview = codereview
1167 self._codereview_impl = cls(self, **kwargs)
1168 self.issue = int(issue)
1169 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170
1171 # No issue is set for this branch, so decide based on repo-wide settings.
1172 return self._load_codereview_impl(
1173 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1174 **kwargs)
1175
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001176 def IsGerrit(self):
1177 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001178
1179 def GetCCList(self):
1180 """Return the users cc'd on this CL.
1181
agable92bec4f2016-08-24 09:27:27 -07001182 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001183 """
1184 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001185 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186 more_cc = ','.join(self.watchers)
1187 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1188 return self.cc
1189
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001190 def GetCCListWithoutDefault(self):
1191 """Return the users cc'd on this CL excluding default ones."""
1192 if self.cc is None:
1193 self.cc = ','.join(self.watchers)
1194 return self.cc
1195
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 def SetWatchers(self, watchers):
1197 """Set the list of email addresses that should be cc'd based on the changed
1198 files in this CL.
1199 """
1200 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
1202 def GetBranch(self):
1203 """Returns the short branch name, e.g. 'master'."""
1204 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001205 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001206 if not branchref:
1207 return None
1208 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 self.branch = ShortBranchName(self.branchref)
1210 return self.branch
1211
1212 def GetBranchRef(self):
1213 """Returns the full branch name, e.g. 'refs/heads/master'."""
1214 self.GetBranch() # Poke the lazy loader.
1215 return self.branchref
1216
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001217 def ClearBranch(self):
1218 """Clears cached branch data of this object."""
1219 self.branch = self.branchref = None
1220
tandrii5d48c322016-08-18 16:19:37 -07001221 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1222 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1223 kwargs['branch'] = self.GetBranch()
1224 return _git_get_branch_config_value(key, default, **kwargs)
1225
1226 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1227 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1228 assert self.GetBranch(), (
1229 'this CL must have an associated branch to %sset %s%s' %
1230 ('un' if value is None else '',
1231 key,
1232 '' if value is None else ' to %r' % value))
1233 kwargs['branch'] = self.GetBranch()
1234 return _git_set_branch_config_value(key, value, **kwargs)
1235
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001236 @staticmethod
1237 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001238 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 e.g. 'origin', 'refs/heads/master'
1240 """
1241 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001242 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001245 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001247 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1248 error_ok=True).strip()
1249 if upstream_branch:
1250 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001252 # Fall back on trying a git-svn upstream branch.
1253 if settings.GetIsGitSvn():
1254 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001256 # Else, try to guess the origin remote.
1257 remote_branches = RunGit(['branch', '-r']).split()
1258 if 'origin/master' in remote_branches:
1259 # Fall back on origin/master if it exits.
1260 remote = 'origin'
1261 upstream_branch = 'refs/heads/master'
1262 elif 'origin/trunk' in remote_branches:
1263 # Fall back on origin/trunk if it exists. Generally a shared
1264 # git-svn clone
1265 remote = 'origin'
1266 upstream_branch = 'refs/heads/trunk'
1267 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001268 DieWithError(
1269 'Unable to determine default branch to diff against.\n'
1270 'Either pass complete "git diff"-style arguments, like\n'
1271 ' git cl upload origin/master\n'
1272 'or verify this branch is set up to track another \n'
1273 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
1275 return remote, upstream_branch
1276
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001277 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001278 upstream_branch = self.GetUpstreamBranch()
1279 if not BranchExists(upstream_branch):
1280 DieWithError('The upstream for the current branch (%s) does not exist '
1281 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001282 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001283 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 def GetUpstreamBranch(self):
1286 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001289 upstream_branch = upstream_branch.replace('refs/heads/',
1290 'refs/remotes/%s/' % remote)
1291 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1292 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 self.upstream_branch = upstream_branch
1294 return self.upstream_branch
1295
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001297 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 remote, branch = None, self.GetBranch()
1299 seen_branches = set()
1300 while branch not in seen_branches:
1301 seen_branches.add(branch)
1302 remote, branch = self.FetchUpstreamTuple(branch)
1303 branch = ShortBranchName(branch)
1304 if remote != '.' or branch.startswith('refs/remotes'):
1305 break
1306 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001307 remotes = RunGit(['remote'], error_ok=True).split()
1308 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 logging.warning('Could not determine which remote this change is '
1313 'associated with, so defaulting to "%s". This may '
1314 'not be what you want. You may prevent this message '
1315 'by running "git svn info" as documented here: %s',
1316 self._remote,
1317 GIT_INSTRUCTIONS_URL)
1318 else:
1319 logging.warn('Could not determine which remote this change is '
1320 'associated with. You may prevent this message by '
1321 'running "git svn info" as documented here: %s',
1322 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 branch = 'HEAD'
1324 if branch.startswith('refs/remotes'):
1325 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001326 elif branch.startswith('refs/branch-heads/'):
1327 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 else:
1329 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001330 return self._remote
1331
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001332 def GitSanityChecks(self, upstream_git_obj):
1333 """Checks git repo status and ensures diff is from local commits."""
1334
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 if upstream_git_obj is None:
1336 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001337 print('ERROR: unable to determine current branch (detached HEAD?)',
1338 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001339 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001340 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001341 return False
1342
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 # Verify the commit we're diffing against is in our current branch.
1344 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1345 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1346 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001347 print('ERROR: %s is not in the current branch. You may need to rebase '
1348 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001349 return False
1350
1351 # List the commits inside the diff, and verify they are all local.
1352 commits_in_diff = RunGit(
1353 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1354 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1355 remote_branch = remote_branch.strip()
1356 if code != 0:
1357 _, remote_branch = self.GetRemoteBranch()
1358
1359 commits_in_remote = RunGit(
1360 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1361
1362 common_commits = set(commits_in_diff) & set(commits_in_remote)
1363 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001364 print('ERROR: Your diff contains %d commits already in %s.\n'
1365 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1366 'the diff. If you are using a custom git flow, you can override'
1367 ' the reference used for this check with "git config '
1368 'gitcl.remotebranch <git-ref>".' % (
1369 len(common_commits), remote_branch, upstream_git_obj),
1370 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001371 return False
1372 return True
1373
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001374 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001375 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001376
1377 Returns None if it is not set.
1378 """
tandrii5d48c322016-08-18 16:19:37 -07001379 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001380
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001381 def GetGitSvnRemoteUrl(self):
1382 """Return the configured git-svn remote URL parsed from git svn info.
1383
1384 Returns None if it is not set.
1385 """
1386 # URL is dependent on the current directory.
1387 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1388 if data:
1389 keys = dict(line.split(': ', 1) for line in data.splitlines()
1390 if ': ' in line)
1391 return keys.get('URL', None)
1392 return None
1393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 def GetRemoteUrl(self):
1395 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1396
1397 Returns None if there is no remote.
1398 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001399 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001400 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1401
1402 # If URL is pointing to a local directory, it is probably a git cache.
1403 if os.path.isdir(url):
1404 url = RunGit(['config', 'remote.%s.url' % remote],
1405 error_ok=True,
1406 cwd=url).strip()
1407 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001409 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001410 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001411 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001412 self.issue = self._GitGetBranchConfigValue(
1413 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 return self.issue
1416
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 def GetIssueURL(self):
1418 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001419 issue = self.GetIssue()
1420 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001421 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001422 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423
1424 def GetDescription(self, pretty=False):
1425 if not self.has_description:
1426 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001427 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428 self.has_description = True
1429 if pretty:
1430 wrapper = textwrap.TextWrapper()
1431 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1432 return wrapper.fill(self.description)
1433 return self.description
1434
1435 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001436 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001438 self.patchset = self._GitGetBranchConfigValue(
1439 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001440 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 return self.patchset
1442
1443 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001444 """Set this branch's patchset. If patchset=0, clears the patchset."""
1445 assert self.GetBranch()
1446 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001447 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001448 else:
1449 self.patchset = int(patchset)
1450 self._GitSetBranchConfigValue(
1451 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001453 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001454 """Set this branch's issue. If issue isn't given, clears the issue."""
1455 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001457 issue = int(issue)
1458 self._GitSetBranchConfigValue(
1459 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001460 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001461 codereview_server = self._codereview_impl.GetCodereviewServer()
1462 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001463 self._GitSetBranchConfigValue(
1464 self._codereview_impl.CodereviewServerConfigKey(),
1465 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 else:
tandrii5d48c322016-08-18 16:19:37 -07001467 # Reset all of these just to be clean.
1468 reset_suffixes = [
1469 'last-upload-hash',
1470 self._codereview_impl.IssueConfigKey(),
1471 self._codereview_impl.PatchsetConfigKey(),
1472 self._codereview_impl.CodereviewServerConfigKey(),
1473 ] + self._PostUnsetIssueProperties()
1474 for prop in reset_suffixes:
1475 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001476 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001477 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478
dnjba1b0f32016-09-02 12:37:42 -07001479 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001480 if not self.GitSanityChecks(upstream_branch):
1481 DieWithError('\nGit sanity check failure')
1482
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001483 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001484 if not root:
1485 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001486 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001487
1488 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001489 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001490 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001491 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001492 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 except subprocess2.CalledProcessError:
1494 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001495 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 'This branch probably doesn\'t exist anymore. To reset the\n'
1497 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001498 ' git branch --set-upstream-to origin/master %s\n'
1499 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001500 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001501
maruel@chromium.org52424302012-08-29 15:14:30 +00001502 issue = self.GetIssue()
1503 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001504 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001505 description = self.GetDescription()
1506 else:
1507 # If the change was never uploaded, use the log messages of all commits
1508 # up to the branch point, as git cl upload will prefill the description
1509 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001510 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1511 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001512
1513 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001514 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001515 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001516 name,
1517 description,
1518 absroot,
1519 files,
1520 issue,
1521 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001522 author,
1523 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524
dsansomee2d6fd92016-09-08 00:10:47 -07001525 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001527 return self._codereview_impl.UpdateDescriptionRemote(
1528 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529
1530 def RunHook(self, committing, may_prompt, verbose, change):
1531 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1532 try:
1533 return presubmit_support.DoPresubmitChecks(change, committing,
1534 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1535 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001536 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1537 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001538 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001539 DieWithError(
1540 ('%s\nMaybe your depot_tools is out of date?\n'
1541 'If all fails, contact maruel@') % e)
1542
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001543 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1544 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001545 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1546 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001547 else:
1548 # Assume url.
1549 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1550 urlparse.urlparse(issue_arg))
1551 if not parsed_issue_arg or not parsed_issue_arg.valid:
1552 DieWithError('Failed to parse issue argument "%s". '
1553 'Must be an issue number or a valid URL.' % issue_arg)
1554 return self._codereview_impl.CMDPatchWithParsedIssue(
1555 parsed_issue_arg, reject, nocommit, directory)
1556
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001557 def CMDUpload(self, options, git_diff_args, orig_args):
1558 """Uploads a change to codereview."""
1559 if git_diff_args:
1560 # TODO(ukai): is it ok for gerrit case?
1561 base_branch = git_diff_args[0]
1562 else:
1563 if self.GetBranch() is None:
1564 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1565
1566 # Default to diffing against common ancestor of upstream branch
1567 base_branch = self.GetCommonAncestorWithUpstream()
1568 git_diff_args = [base_branch, 'HEAD']
1569
1570 # Make sure authenticated to codereview before running potentially expensive
1571 # hooks. It is a fast, best efforts check. Codereview still can reject the
1572 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001573 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001574
1575 # Apply watchlists on upload.
1576 change = self.GetChange(base_branch, None)
1577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1578 files = [f.LocalPath() for f in change.AffectedFiles()]
1579 if not options.bypass_watchlists:
1580 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1581
1582 if not options.bypass_hooks:
1583 if options.reviewers or options.tbr_owners:
1584 # Set the reviewer list now so that presubmit checks can access it.
1585 change_description = ChangeDescription(change.FullDescriptionText())
1586 change_description.update_reviewers(options.reviewers,
1587 options.tbr_owners,
1588 change)
1589 change.SetDescriptionText(change_description.description)
1590 hook_results = self.RunHook(committing=False,
1591 may_prompt=not options.force,
1592 verbose=options.verbose,
1593 change=change)
1594 if not hook_results.should_continue():
1595 return 1
1596 if not options.reviewers and hook_results.reviewers:
1597 options.reviewers = hook_results.reviewers.split(',')
1598
1599 if self.GetIssue():
1600 latest_patchset = self.GetMostRecentPatchset()
1601 local_patchset = self.GetPatchset()
1602 if (latest_patchset and local_patchset and
1603 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001604 print('The last upload made from this repository was patchset #%d but '
1605 'the most recent patchset on the server is #%d.'
1606 % (local_patchset, latest_patchset))
1607 print('Uploading will still work, but if you\'ve uploaded to this '
1608 'issue from another machine or branch the patch you\'re '
1609 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 ask_for_data('About to upload; enter to confirm.')
1611
1612 print_stats(options.similarity, options.find_copies, git_diff_args)
1613 ret = self.CMDUploadChange(options, git_diff_args, change)
1614 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001615 if options.use_commit_queue:
1616 self.SetCQState(_CQState.COMMIT)
1617 elif options.cq_dry_run:
1618 self.SetCQState(_CQState.DRY_RUN)
1619
tandrii5d48c322016-08-18 16:19:37 -07001620 _git_set_branch_config_value('last-upload-hash',
1621 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001622 # Run post upload hooks, if specified.
1623 if settings.GetRunPostUploadHook():
1624 presubmit_support.DoPostUploadExecuter(
1625 change,
1626 self,
1627 settings.GetRoot(),
1628 options.verbose,
1629 sys.stdout)
1630
1631 # Upload all dependencies if specified.
1632 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001633 print()
1634 print('--dependencies has been specified.')
1635 print('All dependent local branches will be re-uploaded.')
1636 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637 # Remove the dependencies flag from args so that we do not end up in a
1638 # loop.
1639 orig_args.remove('--dependencies')
1640 ret = upload_branch_deps(self, orig_args)
1641 return ret
1642
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001643 def SetCQState(self, new_state):
1644 """Update the CQ state for latest patchset.
1645
1646 Issue must have been already uploaded and known.
1647 """
1648 assert new_state in _CQState.ALL_STATES
1649 assert self.GetIssue()
1650 return self._codereview_impl.SetCQState(new_state)
1651
qyearsley1fdfcb62016-10-24 13:22:03 -07001652 def TriggerDryRun(self):
1653 """Triggers a dry run and prints a warning on failure."""
1654 # TODO(qyearsley): Either re-use this method in CMDset_commit
1655 # and CMDupload, or change CMDtry to trigger dry runs with
1656 # just SetCQState, and catch keyboard interrupt and other
1657 # errors in that method.
1658 try:
1659 self.SetCQState(_CQState.DRY_RUN)
1660 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1661 return 0
1662 except KeyboardInterrupt:
1663 raise
1664 except:
1665 print('WARNING: failed to trigger CQ Dry Run.\n'
1666 'Either:\n'
1667 ' * your project has no CQ\n'
1668 ' * you don\'t have permission to trigger Dry Run\n'
1669 ' * bug in this code (see stack trace below).\n'
1670 'Consider specifying which bots to trigger manually '
1671 'or asking your project owners for permissions '
1672 'or contacting Chrome Infrastructure team at '
1673 'https://www.chromium.org/infra\n\n')
1674 # Still raise exception so that stack trace is printed.
1675 raise
1676
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677 # Forward methods to codereview specific implementation.
1678
1679 def CloseIssue(self):
1680 return self._codereview_impl.CloseIssue()
1681
1682 def GetStatus(self):
1683 return self._codereview_impl.GetStatus()
1684
1685 def GetCodereviewServer(self):
1686 return self._codereview_impl.GetCodereviewServer()
1687
tandriide281ae2016-10-12 06:02:30 -07001688 def GetIssueOwner(self):
1689 """Get owner from codereview, which may differ from this checkout."""
1690 return self._codereview_impl.GetIssueOwner()
1691
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692 def GetApprovingReviewers(self):
1693 return self._codereview_impl.GetApprovingReviewers()
1694
1695 def GetMostRecentPatchset(self):
1696 return self._codereview_impl.GetMostRecentPatchset()
1697
tandriide281ae2016-10-12 06:02:30 -07001698 def CannotTriggerTryJobReason(self):
1699 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1700 return self._codereview_impl.CannotTriggerTryJobReason()
1701
tandrii8c5a3532016-11-04 07:52:02 -07001702 def GetTryjobProperties(self, patchset=None):
1703 """Returns dictionary of properties to launch tryjob."""
1704 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1705
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001706 def __getattr__(self, attr):
1707 # This is because lots of untested code accesses Rietveld-specific stuff
1708 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001709 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001710 # Note that child method defines __getattr__ as well, and forwards it here,
1711 # because _RietveldChangelistImpl is not cleaned up yet, and given
1712 # deprecation of Rietveld, it should probably be just removed.
1713 # Until that time, avoid infinite recursion by bypassing __getattr__
1714 # of implementation class.
1715 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001716
1717
1718class _ChangelistCodereviewBase(object):
1719 """Abstract base class encapsulating codereview specifics of a changelist."""
1720 def __init__(self, changelist):
1721 self._changelist = changelist # instance of Changelist
1722
1723 def __getattr__(self, attr):
1724 # Forward methods to changelist.
1725 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1726 # _RietveldChangelistImpl to avoid this hack?
1727 return getattr(self._changelist, attr)
1728
1729 def GetStatus(self):
1730 """Apply a rough heuristic to give a simple summary of an issue's review
1731 or CQ status, assuming adherence to a common workflow.
1732
1733 Returns None if no issue for this branch, or specific string keywords.
1734 """
1735 raise NotImplementedError()
1736
1737 def GetCodereviewServer(self):
1738 """Returns server URL without end slash, like "https://codereview.com"."""
1739 raise NotImplementedError()
1740
1741 def FetchDescription(self):
1742 """Fetches and returns description from the codereview server."""
1743 raise NotImplementedError()
1744
tandrii5d48c322016-08-18 16:19:37 -07001745 @classmethod
1746 def IssueConfigKey(cls):
1747 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 raise NotImplementedError()
1749
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001750 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001751 def PatchsetConfigKey(cls):
1752 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 raise NotImplementedError()
1754
tandrii5d48c322016-08-18 16:19:37 -07001755 @classmethod
1756 def CodereviewServerConfigKey(cls):
1757 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 raise NotImplementedError()
1759
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001760 def _PostUnsetIssueProperties(self):
1761 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001762 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001763
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 def GetRieveldObjForPresubmit(self):
1765 # This is an unfortunate Rietveld-embeddedness in presubmit.
1766 # For non-Rietveld codereviews, this probably should return a dummy object.
1767 raise NotImplementedError()
1768
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001769 def GetGerritObjForPresubmit(self):
1770 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1771 return None
1772
dsansomee2d6fd92016-09-08 00:10:47 -07001773 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001774 """Update the description on codereview site."""
1775 raise NotImplementedError()
1776
1777 def CloseIssue(self):
1778 """Closes the issue."""
1779 raise NotImplementedError()
1780
1781 def GetApprovingReviewers(self):
1782 """Returns a list of reviewers approving the change.
1783
1784 Note: not necessarily committers.
1785 """
1786 raise NotImplementedError()
1787
1788 def GetMostRecentPatchset(self):
1789 """Returns the most recent patchset number from the codereview site."""
1790 raise NotImplementedError()
1791
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001792 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1793 directory):
1794 """Fetches and applies the issue.
1795
1796 Arguments:
1797 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1798 reject: if True, reject the failed patch instead of switching to 3-way
1799 merge. Rietveld only.
1800 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1801 only.
1802 directory: switch to directory before applying the patch. Rietveld only.
1803 """
1804 raise NotImplementedError()
1805
1806 @staticmethod
1807 def ParseIssueURL(parsed_url):
1808 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1809 failed."""
1810 raise NotImplementedError()
1811
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001812 def EnsureAuthenticated(self, force):
1813 """Best effort check that user is authenticated with codereview server.
1814
1815 Arguments:
1816 force: whether to skip confirmation questions.
1817 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001818 raise NotImplementedError()
1819
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001820 def CMDUploadChange(self, options, args, change):
1821 """Uploads a change to codereview."""
1822 raise NotImplementedError()
1823
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001824 def SetCQState(self, new_state):
1825 """Update the CQ state for latest patchset.
1826
1827 Issue must have been already uploaded and known.
1828 """
1829 raise NotImplementedError()
1830
tandriie113dfd2016-10-11 10:20:12 -07001831 def CannotTriggerTryJobReason(self):
1832 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1833 raise NotImplementedError()
1834
tandriide281ae2016-10-12 06:02:30 -07001835 def GetIssueOwner(self):
1836 raise NotImplementedError()
1837
tandrii8c5a3532016-11-04 07:52:02 -07001838 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001839 raise NotImplementedError()
1840
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001841
1842class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1843 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1844 super(_RietveldChangelistImpl, self).__init__(changelist)
1845 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001846 if not rietveld_server:
1847 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848
1849 self._rietveld_server = rietveld_server
1850 self._auth_config = auth_config
1851 self._props = None
1852 self._rpc_server = None
1853
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001854 def GetCodereviewServer(self):
1855 if not self._rietveld_server:
1856 # If we're on a branch then get the server potentially associated
1857 # with that branch.
1858 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001859 self._rietveld_server = gclient_utils.UpgradeToHttps(
1860 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001861 if not self._rietveld_server:
1862 self._rietveld_server = settings.GetDefaultServerUrl()
1863 return self._rietveld_server
1864
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001865 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001866 """Best effort check that user is authenticated with Rietveld server."""
1867 if self._auth_config.use_oauth2:
1868 authenticator = auth.get_authenticator_for_host(
1869 self.GetCodereviewServer(), self._auth_config)
1870 if not authenticator.has_cached_credentials():
1871 raise auth.LoginRequiredError(self.GetCodereviewServer())
1872
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001873 def FetchDescription(self):
1874 issue = self.GetIssue()
1875 assert issue
1876 try:
1877 return self.RpcServer().get_description(issue).strip()
1878 except urllib2.HTTPError as e:
1879 if e.code == 404:
1880 DieWithError(
1881 ('\nWhile fetching the description for issue %d, received a '
1882 '404 (not found)\n'
1883 'error. It is likely that you deleted this '
1884 'issue on the server. If this is the\n'
1885 'case, please run\n\n'
1886 ' git cl issue 0\n\n'
1887 'to clear the association with the deleted issue. Then run '
1888 'this command again.') % issue)
1889 else:
1890 DieWithError(
1891 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1892 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001893 print('Warning: Failed to retrieve CL description due to network '
1894 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001895 return ''
1896
1897 def GetMostRecentPatchset(self):
1898 return self.GetIssueProperties()['patchsets'][-1]
1899
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001900 def GetIssueProperties(self):
1901 if self._props is None:
1902 issue = self.GetIssue()
1903 if not issue:
1904 self._props = {}
1905 else:
1906 self._props = self.RpcServer().get_issue_properties(issue, True)
1907 return self._props
1908
tandriie113dfd2016-10-11 10:20:12 -07001909 def CannotTriggerTryJobReason(self):
1910 props = self.GetIssueProperties()
1911 if not props:
1912 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1913 if props.get('closed'):
1914 return 'CL %s is closed' % self.GetIssue()
1915 if props.get('private'):
1916 return 'CL %s is private' % self.GetIssue()
1917 return None
1918
tandrii8c5a3532016-11-04 07:52:02 -07001919 def GetTryjobProperties(self, patchset=None):
1920 """Returns dictionary of properties to launch tryjob."""
1921 project = (self.GetIssueProperties() or {}).get('project')
1922 return {
1923 'issue': self.GetIssue(),
1924 'patch_project': project,
1925 'patch_storage': 'rietveld',
1926 'patchset': patchset or self.GetPatchset(),
1927 'rietveld': self.GetCodereviewServer(),
1928 }
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 def GetApprovingReviewers(self):
1931 return get_approving_reviewers(self.GetIssueProperties())
1932
tandriide281ae2016-10-12 06:02:30 -07001933 def GetIssueOwner(self):
1934 return (self.GetIssueProperties() or {}).get('owner_email')
1935
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001936 def AddComment(self, message):
1937 return self.RpcServer().add_comment(self.GetIssue(), message)
1938
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001939 def GetStatus(self):
1940 """Apply a rough heuristic to give a simple summary of an issue's review
1941 or CQ status, assuming adherence to a common workflow.
1942
1943 Returns None if no issue for this branch, or one of the following keywords:
1944 * 'error' - error from review tool (including deleted issues)
1945 * 'unsent' - not sent for review
1946 * 'waiting' - waiting for review
1947 * 'reply' - waiting for owner to reply to review
1948 * 'lgtm' - LGTM from at least one approved reviewer
1949 * 'commit' - in the commit queue
1950 * 'closed' - closed
1951 """
1952 if not self.GetIssue():
1953 return None
1954
1955 try:
1956 props = self.GetIssueProperties()
1957 except urllib2.HTTPError:
1958 return 'error'
1959
1960 if props.get('closed'):
1961 # Issue is closed.
1962 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001963 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001964 # Issue is in the commit queue.
1965 return 'commit'
1966
1967 try:
1968 reviewers = self.GetApprovingReviewers()
1969 except urllib2.HTTPError:
1970 return 'error'
1971
1972 if reviewers:
1973 # Was LGTM'ed.
1974 return 'lgtm'
1975
1976 messages = props.get('messages') or []
1977
tandrii9d2c7a32016-06-22 03:42:45 -07001978 # Skip CQ messages that don't require owner's action.
1979 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1980 if 'Dry run:' in messages[-1]['text']:
1981 messages.pop()
1982 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1983 # This message always follows prior messages from CQ,
1984 # so skip this too.
1985 messages.pop()
1986 else:
1987 # This is probably a CQ messages warranting user attention.
1988 break
1989
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001990 if not messages:
1991 # No message was sent.
1992 return 'unsent'
1993 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001994 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001995 return 'reply'
1996 return 'waiting'
1997
dsansomee2d6fd92016-09-08 00:10:47 -07001998 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001999 return self.RpcServer().update_description(
2000 self.GetIssue(), self.description)
2001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002003 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002005 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002006 return self.SetFlags({flag: value})
2007
2008 def SetFlags(self, flags):
2009 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002010 """
phajdan.jr68598232016-08-10 03:28:28 -07002011 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002012 try:
tandrii4b233bd2016-07-06 03:50:29 -07002013 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002014 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002015 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002016 if e.code == 404:
2017 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2018 if e.code == 403:
2019 DieWithError(
2020 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002021 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002022 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002024 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025 """Returns an upload.RpcServer() to access this review's rietveld instance.
2026 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002027 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002028 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002029 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002030 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002031 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002033 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002034 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002035 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036
tandrii5d48c322016-08-18 16:19:37 -07002037 @classmethod
2038 def PatchsetConfigKey(cls):
2039 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040
tandrii5d48c322016-08-18 16:19:37 -07002041 @classmethod
2042 def CodereviewServerConfigKey(cls):
2043 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002044
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 def GetRieveldObjForPresubmit(self):
2046 return self.RpcServer()
2047
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002048 def SetCQState(self, new_state):
2049 props = self.GetIssueProperties()
2050 if props.get('private'):
2051 DieWithError('Cannot set-commit on private issue')
2052
2053 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002054 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002055 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002056 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002057 else:
tandrii4b233bd2016-07-06 03:50:29 -07002058 assert new_state == _CQState.DRY_RUN
2059 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002060
2061
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002062 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2063 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002064 # PatchIssue should never be called with a dirty tree. It is up to the
2065 # caller to check this, but just in case we assert here since the
2066 # consequences of the caller not checking this could be dire.
2067 assert(not git_common.is_dirty_git_tree('apply'))
2068 assert(parsed_issue_arg.valid)
2069 self._changelist.issue = parsed_issue_arg.issue
2070 if parsed_issue_arg.hostname:
2071 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2072
skobes6468b902016-10-24 08:45:10 -07002073 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2074 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2075 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002076 try:
skobes6468b902016-10-24 08:45:10 -07002077 scm_obj.apply_patch(patchset_object)
2078 except Exception as e:
2079 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002080 return 1
2081
2082 # If we had an issue, commit the current state and register the issue.
2083 if not nocommit:
2084 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2085 'patch from issue %(i)s at patchset '
2086 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2087 % {'i': self.GetIssue(), 'p': patchset})])
2088 self.SetIssue(self.GetIssue())
2089 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002090 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002091 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002092 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002093 return 0
2094
2095 @staticmethod
2096 def ParseIssueURL(parsed_url):
2097 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2098 return None
wychen3c1c1722016-08-04 11:46:36 -07002099 # Rietveld patch: https://domain/<number>/#ps<patchset>
2100 match = re.match(r'/(\d+)/$', parsed_url.path)
2101 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2102 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002103 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002104 issue=int(match.group(1)),
2105 patchset=int(match2.group(1)),
2106 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 # Typical url: https://domain/<issue_number>[/[other]]
2108 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2109 if match:
skobes6468b902016-10-24 08:45:10 -07002110 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002111 issue=int(match.group(1)),
2112 hostname=parsed_url.netloc)
2113 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2114 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2115 if match:
skobes6468b902016-10-24 08:45:10 -07002116 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002117 issue=int(match.group(1)),
2118 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002119 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002120 return None
2121
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122 def CMDUploadChange(self, options, args, change):
2123 """Upload the patch to Rietveld."""
2124 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2125 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002126 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2127 if options.emulate_svn_auto_props:
2128 upload_args.append('--emulate_svn_auto_props')
2129
2130 change_desc = None
2131
2132 if options.email is not None:
2133 upload_args.extend(['--email', options.email])
2134
2135 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002136 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002137 upload_args.extend(['--title', options.title])
2138 if options.message:
2139 upload_args.extend(['--message', options.message])
2140 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002141 print('This branch is associated with issue %s. '
2142 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 else:
nodirca166002016-06-27 10:59:51 -07002144 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002145 upload_args.extend(['--title', options.title])
2146 message = (options.title or options.message or
2147 CreateDescriptionFromLog(args))
2148 change_desc = ChangeDescription(message)
2149 if options.reviewers or options.tbr_owners:
2150 change_desc.update_reviewers(options.reviewers,
2151 options.tbr_owners,
2152 change)
2153 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002154 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155
2156 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 return 1
2159
2160 upload_args.extend(['--message', change_desc.description])
2161 if change_desc.get_reviewers():
2162 upload_args.append('--reviewers=%s' % ','.join(
2163 change_desc.get_reviewers()))
2164 if options.send_mail:
2165 if not change_desc.get_reviewers():
2166 DieWithError("Must specify reviewers to send email.")
2167 upload_args.append('--send_mail')
2168
2169 # We check this before applying rietveld.private assuming that in
2170 # rietveld.cc only addresses which we can send private CLs to are listed
2171 # if rietveld.private is set, and so we should ignore rietveld.cc only
2172 # when --private is specified explicitly on the command line.
2173 if options.private:
2174 logging.warn('rietveld.cc is ignored since private flag is specified. '
2175 'You need to review and add them manually if necessary.')
2176 cc = self.GetCCListWithoutDefault()
2177 else:
2178 cc = self.GetCCList()
2179 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002180 if change_desc.get_cced():
2181 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002182 if cc:
2183 upload_args.extend(['--cc', cc])
2184
2185 if options.private or settings.GetDefaultPrivateFlag() == "True":
2186 upload_args.append('--private')
2187
2188 upload_args.extend(['--git_similarity', str(options.similarity)])
2189 if not options.find_copies:
2190 upload_args.extend(['--git_no_find_copies'])
2191
2192 # Include the upstream repo's URL in the change -- this is useful for
2193 # projects that have their source spread across multiple repos.
2194 remote_url = self.GetGitBaseUrlFromConfig()
2195 if not remote_url:
2196 if settings.GetIsGitSvn():
2197 remote_url = self.GetGitSvnRemoteUrl()
2198 else:
2199 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2200 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2201 self.GetUpstreamBranch().split('/')[-1])
2202 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002203 remote, remote_branch = self.GetRemoteBranch()
2204 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2205 settings.GetPendingRefPrefix())
2206 if target_ref:
2207 upload_args.extend(['--target_ref', target_ref])
2208
2209 # Look for dependent patchsets. See crbug.com/480453 for more details.
2210 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2211 upstream_branch = ShortBranchName(upstream_branch)
2212 if remote is '.':
2213 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002214 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002215 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002216 print()
2217 print('Skipping dependency patchset upload because git config '
2218 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2219 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 else:
2221 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002222 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 auth_config=auth_config)
2224 branch_cl_issue_url = branch_cl.GetIssueURL()
2225 branch_cl_issue = branch_cl.GetIssue()
2226 branch_cl_patchset = branch_cl.GetPatchset()
2227 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2228 upload_args.extend(
2229 ['--depends_on_patchset', '%s:%s' % (
2230 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002231 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002232 '\n'
2233 'The current branch (%s) is tracking a local branch (%s) with '
2234 'an associated CL.\n'
2235 'Adding %s/#ps%s as a dependency patchset.\n'
2236 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2237 branch_cl_patchset))
2238
2239 project = settings.GetProject()
2240 if project:
2241 upload_args.extend(['--project', project])
2242
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243 try:
2244 upload_args = ['upload'] + upload_args + args
2245 logging.info('upload.RealMain(%s)', upload_args)
2246 issue, patchset = upload.RealMain(upload_args)
2247 issue = int(issue)
2248 patchset = int(patchset)
2249 except KeyboardInterrupt:
2250 sys.exit(1)
2251 except:
2252 # If we got an exception after the user typed a description for their
2253 # change, back up the description before re-raising.
2254 if change_desc:
2255 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2256 print('\nGot exception while uploading -- saving description to %s\n' %
2257 backup_path)
2258 backup_file = open(backup_path, 'w')
2259 backup_file.write(change_desc.description)
2260 backup_file.close()
2261 raise
2262
2263 if not self.GetIssue():
2264 self.SetIssue(issue)
2265 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002266 return 0
2267
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002268
2269class _GerritChangelistImpl(_ChangelistCodereviewBase):
2270 def __init__(self, changelist, auth_config=None):
2271 # auth_config is Rietveld thing, kept here to preserve interface only.
2272 super(_GerritChangelistImpl, self).__init__(changelist)
2273 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002274 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002275 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002276 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002277
2278 def _GetGerritHost(self):
2279 # Lazy load of configs.
2280 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002281 if self._gerrit_host and '.' not in self._gerrit_host:
2282 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2283 # This happens for internal stuff http://crbug.com/614312.
2284 parsed = urlparse.urlparse(self.GetRemoteUrl())
2285 if parsed.scheme == 'sso':
2286 print('WARNING: using non https URLs for remote is likely broken\n'
2287 ' Your current remote is: %s' % self.GetRemoteUrl())
2288 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2289 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 return self._gerrit_host
2291
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002292 def _GetGitHost(self):
2293 """Returns git host to be used when uploading change to Gerrit."""
2294 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2295
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002296 def GetCodereviewServer(self):
2297 if not self._gerrit_server:
2298 # If we're on a branch then get the server potentially associated
2299 # with that branch.
2300 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002301 self._gerrit_server = self._GitGetBranchConfigValue(
2302 self.CodereviewServerConfigKey())
2303 if self._gerrit_server:
2304 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002305 if not self._gerrit_server:
2306 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2307 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002308 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002309 parts[0] = parts[0] + '-review'
2310 self._gerrit_host = '.'.join(parts)
2311 self._gerrit_server = 'https://%s' % self._gerrit_host
2312 return self._gerrit_server
2313
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002314 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002315 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002316 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002317
tandrii5d48c322016-08-18 16:19:37 -07002318 @classmethod
2319 def PatchsetConfigKey(cls):
2320 return 'gerritpatchset'
2321
2322 @classmethod
2323 def CodereviewServerConfigKey(cls):
2324 return 'gerritserver'
2325
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002326 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002327 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002328 if settings.GetGerritSkipEnsureAuthenticated():
2329 # For projects with unusual authentication schemes.
2330 # See http://crbug.com/603378.
2331 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002332 # Lazy-loader to identify Gerrit and Git hosts.
2333 if gerrit_util.GceAuthenticator.is_gce():
2334 return
2335 self.GetCodereviewServer()
2336 git_host = self._GetGitHost()
2337 assert self._gerrit_server and self._gerrit_host
2338 cookie_auth = gerrit_util.CookiesAuthenticator()
2339
2340 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2341 git_auth = cookie_auth.get_auth_header(git_host)
2342 if gerrit_auth and git_auth:
2343 if gerrit_auth == git_auth:
2344 return
2345 print((
2346 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2347 ' Check your %s or %s file for credentials of hosts:\n'
2348 ' %s\n'
2349 ' %s\n'
2350 ' %s') %
2351 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2352 git_host, self._gerrit_host,
2353 cookie_auth.get_new_password_message(git_host)))
2354 if not force:
2355 ask_for_data('If you know what you are doing, press Enter to continue, '
2356 'Ctrl+C to abort.')
2357 return
2358 else:
2359 missing = (
2360 [] if gerrit_auth else [self._gerrit_host] +
2361 [] if git_auth else [git_host])
2362 DieWithError('Credentials for the following hosts are required:\n'
2363 ' %s\n'
2364 'These are read from %s (or legacy %s)\n'
2365 '%s' % (
2366 '\n '.join(missing),
2367 cookie_auth.get_gitcookies_path(),
2368 cookie_auth.get_netrc_path(),
2369 cookie_auth.get_new_password_message(git_host)))
2370
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002371 def _PostUnsetIssueProperties(self):
2372 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002373 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002374
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375 def GetRieveldObjForPresubmit(self):
2376 class ThisIsNotRietveldIssue(object):
2377 def __nonzero__(self):
2378 # This is a hack to make presubmit_support think that rietveld is not
2379 # defined, yet still ensure that calls directly result in a decent
2380 # exception message below.
2381 return False
2382
2383 def __getattr__(self, attr):
2384 print(
2385 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2386 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2387 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2388 'or use Rietveld for codereview.\n'
2389 'See also http://crbug.com/579160.' % attr)
2390 raise NotImplementedError()
2391 return ThisIsNotRietveldIssue()
2392
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002393 def GetGerritObjForPresubmit(self):
2394 return presubmit_support.GerritAccessor(self._GetGerritHost())
2395
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002396 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002397 """Apply a rough heuristic to give a simple summary of an issue's review
2398 or CQ status, assuming adherence to a common workflow.
2399
2400 Returns None if no issue for this branch, or one of the following keywords:
2401 * 'error' - error from review tool (including deleted issues)
2402 * 'unsent' - no reviewers added
2403 * 'waiting' - waiting for review
2404 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002405 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2406 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002407 * 'commit' - in the commit queue
2408 * 'closed' - abandoned
2409 """
2410 if not self.GetIssue():
2411 return None
2412
2413 try:
2414 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002415 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002416 return 'error'
2417
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002418 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002419 return 'closed'
2420
2421 cq_label = data['labels'].get('Commit-Queue', {})
2422 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002423 votes = cq_label.get('all', [])
2424 highest_vote = 0
2425 for v in votes:
2426 highest_vote = max(highest_vote, v.get('value', 0))
2427 vote_value = str(highest_vote)
2428 if vote_value != '0':
2429 # Add a '+' if the value is not 0 to match the values in the label.
2430 # The cq_label does not have negatives.
2431 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002432 vote_text = cq_label.get('values', {}).get(vote_value, '')
2433 if vote_text.lower() == 'commit':
2434 return 'commit'
2435
2436 lgtm_label = data['labels'].get('Code-Review', {})
2437 if lgtm_label:
2438 if 'rejected' in lgtm_label:
2439 return 'not lgtm'
2440 if 'approved' in lgtm_label:
2441 return 'lgtm'
2442
2443 if not data.get('reviewers', {}).get('REVIEWER', []):
2444 return 'unsent'
2445
2446 messages = data.get('messages', [])
2447 if messages:
2448 owner = data['owner'].get('_account_id')
2449 last_message_author = messages[-1].get('author', {}).get('_account_id')
2450 if owner != last_message_author:
2451 # Some reply from non-owner.
2452 return 'reply'
2453
2454 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002455
2456 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002457 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002458 return data['revisions'][data['current_revision']]['_number']
2459
2460 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002461 data = self._GetChangeDetail(['CURRENT_REVISION'])
2462 current_rev = data['current_revision']
2463 url = data['revisions'][current_rev]['fetch']['http']['url']
2464 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002465
dsansomee2d6fd92016-09-08 00:10:47 -07002466 def UpdateDescriptionRemote(self, description, force=False):
2467 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2468 if not force:
2469 ask_for_data(
2470 'The description cannot be modified while the issue has a pending '
2471 'unpublished edit. Either publish the edit in the Gerrit web UI '
2472 'or delete it.\n\n'
2473 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2474
2475 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2476 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002477 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2478 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002479
2480 def CloseIssue(self):
2481 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2482
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002483 def GetApprovingReviewers(self):
2484 """Returns a list of reviewers approving the change.
2485
2486 Note: not necessarily committers.
2487 """
2488 raise NotImplementedError()
2489
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002490 def SubmitIssue(self, wait_for_merge=True):
2491 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2492 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002493
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002494 def _GetChangeDetail(self, options=None, issue=None):
2495 options = options or []
2496 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002497 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002498 try:
2499 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2500 options, ignore_404=False)
2501 except gerrit_util.GerritError as e:
2502 if e.http_status == 404:
2503 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2504 raise
tandriic2405f52016-10-10 08:13:15 -07002505 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002506
agable32978d92016-11-01 12:55:02 -07002507 def _GetChangeCommit(self, issue=None):
2508 issue = issue or self.GetIssue()
2509 assert issue, 'issue is required to query Gerrit'
2510 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2511 if not data:
2512 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2513 return data
2514
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002515 def CMDLand(self, force, bypass_hooks, verbose):
2516 if git_common.is_dirty_git_tree('land'):
2517 return 1
tandriid60367b2016-06-22 05:25:12 -07002518 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2519 if u'Commit-Queue' in detail.get('labels', {}):
2520 if not force:
2521 ask_for_data('\nIt seems this repository has a Commit Queue, '
2522 'which can test and land changes for you. '
2523 'Are you sure you wish to bypass it?\n'
2524 'Press Enter to continue, Ctrl+C to abort.')
2525
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002526 differs = True
tandriic4344b52016-08-29 06:04:54 -07002527 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002528 # Note: git diff outputs nothing if there is no diff.
2529 if not last_upload or RunGit(['diff', last_upload]).strip():
2530 print('WARNING: some changes from local branch haven\'t been uploaded')
2531 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002532 if detail['current_revision'] == last_upload:
2533 differs = False
2534 else:
2535 print('WARNING: local branch contents differ from latest uploaded '
2536 'patchset')
2537 if differs:
2538 if not force:
2539 ask_for_data(
2540 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2541 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2542 elif not bypass_hooks:
2543 hook_results = self.RunHook(
2544 committing=True,
2545 may_prompt=not force,
2546 verbose=verbose,
2547 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2548 if not hook_results.should_continue():
2549 return 1
2550
2551 self.SubmitIssue(wait_for_merge=True)
2552 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002553 links = self._GetChangeCommit().get('web_links', [])
2554 for link in links:
2555 if link.get('name') == 'gerrit' and link.get('url'):
2556 print('Landed as %s' % link.get('url'))
2557 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 return 0
2559
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002560 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2561 directory):
2562 assert not reject
2563 assert not nocommit
2564 assert not directory
2565 assert parsed_issue_arg.valid
2566
2567 self._changelist.issue = parsed_issue_arg.issue
2568
2569 if parsed_issue_arg.hostname:
2570 self._gerrit_host = parsed_issue_arg.hostname
2571 self._gerrit_server = 'https://%s' % self._gerrit_host
2572
tandriic2405f52016-10-10 08:13:15 -07002573 try:
2574 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2575 except GerritIssueNotExists as e:
2576 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002577
2578 if not parsed_issue_arg.patchset:
2579 # Use current revision by default.
2580 revision_info = detail['revisions'][detail['current_revision']]
2581 patchset = int(revision_info['_number'])
2582 else:
2583 patchset = parsed_issue_arg.patchset
2584 for revision_info in detail['revisions'].itervalues():
2585 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2586 break
2587 else:
2588 DieWithError('Couldn\'t find patchset %i in issue %i' %
2589 (parsed_issue_arg.patchset, self.GetIssue()))
2590
2591 fetch_info = revision_info['fetch']['http']
2592 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2593 RunGit(['cherry-pick', 'FETCH_HEAD'])
2594 self.SetIssue(self.GetIssue())
2595 self.SetPatchset(patchset)
2596 print('Committed patch for issue %i pathset %i locally' %
2597 (self.GetIssue(), self.GetPatchset()))
2598 return 0
2599
2600 @staticmethod
2601 def ParseIssueURL(parsed_url):
2602 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2603 return None
2604 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2605 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2606 # Short urls like https://domain/<issue_number> can be used, but don't allow
2607 # specifying the patchset (you'd 404), but we allow that here.
2608 if parsed_url.path == '/':
2609 part = parsed_url.fragment
2610 else:
2611 part = parsed_url.path
2612 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2613 if match:
2614 return _ParsedIssueNumberArgument(
2615 issue=int(match.group(2)),
2616 patchset=int(match.group(4)) if match.group(4) else None,
2617 hostname=parsed_url.netloc)
2618 return None
2619
tandrii16e0b4e2016-06-07 10:34:28 -07002620 def _GerritCommitMsgHookCheck(self, offer_removal):
2621 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2622 if not os.path.exists(hook):
2623 return
2624 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2625 # custom developer made one.
2626 data = gclient_utils.FileRead(hook)
2627 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2628 return
2629 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002630 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002631 'and may interfere with it in subtle ways.\n'
2632 'We recommend you remove the commit-msg hook.')
2633 if offer_removal:
2634 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2635 if reply.lower().startswith('y'):
2636 gclient_utils.rm_file_or_tree(hook)
2637 print('Gerrit commit-msg hook removed.')
2638 else:
2639 print('OK, will keep Gerrit commit-msg hook in place.')
2640
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 def CMDUploadChange(self, options, args, change):
2642 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002643 if options.squash and options.no_squash:
2644 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002645
2646 if not options.squash and not options.no_squash:
2647 # Load default for user, repo, squash=true, in this order.
2648 options.squash = settings.GetSquashGerritUploads()
2649 elif options.no_squash:
2650 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002651
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002652 # We assume the remote called "origin" is the one we want.
2653 # It is probably not worthwhile to support different workflows.
2654 gerrit_remote = 'origin'
2655
2656 remote, remote_branch = self.GetRemoteBranch()
2657 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2658 pending_prefix='')
2659
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002661 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 if self.GetIssue():
2663 # Try to get the message from a previous upload.
2664 message = self.GetDescription()
2665 if not message:
2666 DieWithError(
2667 'failed to fetch description from current Gerrit issue %d\n'
2668 '%s' % (self.GetIssue(), self.GetIssueURL()))
2669 change_id = self._GetChangeDetail()['change_id']
2670 while True:
2671 footer_change_ids = git_footers.get_footer_change_id(message)
2672 if footer_change_ids == [change_id]:
2673 break
2674 if not footer_change_ids:
2675 message = git_footers.add_footer_change_id(message, change_id)
2676 print('WARNING: appended missing Change-Id to issue description')
2677 continue
2678 # There is already a valid footer but with different or several ids.
2679 # Doing this automatically is non-trivial as we don't want to lose
2680 # existing other footers, yet we want to append just 1 desired
2681 # Change-Id. Thus, just create a new footer, but let user verify the
2682 # new description.
2683 message = '%s\n\nChange-Id: %s' % (message, change_id)
2684 print(
2685 'WARNING: issue %s has Change-Id footer(s):\n'
2686 ' %s\n'
2687 'but issue has Change-Id %s, according to Gerrit.\n'
2688 'Please, check the proposed correction to the description, '
2689 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2690 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2691 change_id))
2692 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2693 if not options.force:
2694 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002695 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002696 message = change_desc.description
2697 if not message:
2698 DieWithError("Description is empty. Aborting...")
2699 # Continue the while loop.
2700 # Sanity check of this code - we should end up with proper message
2701 # footer.
2702 assert [change_id] == git_footers.get_footer_change_id(message)
2703 change_desc = ChangeDescription(message)
2704 else:
2705 change_desc = ChangeDescription(
2706 options.message or CreateDescriptionFromLog(args))
2707 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002708 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 if not change_desc.description:
2710 DieWithError("Description is empty. Aborting...")
2711 message = change_desc.description
2712 change_ids = git_footers.get_footer_change_id(message)
2713 if len(change_ids) > 1:
2714 DieWithError('too many Change-Id footers, at most 1 allowed.')
2715 if not change_ids:
2716 # Generate the Change-Id automatically.
2717 message = git_footers.add_footer_change_id(
2718 message, GenerateGerritChangeId(message))
2719 change_desc.set_description(message)
2720 change_ids = git_footers.get_footer_change_id(message)
2721 assert len(change_ids) == 1
2722 change_id = change_ids[0]
2723
2724 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2725 if remote is '.':
2726 # If our upstream branch is local, we base our squashed commit on its
2727 # squashed version.
2728 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2729 # Check the squashed hash of the parent.
2730 parent = RunGit(['config',
2731 'branch.%s.gerritsquashhash' % upstream_branch_name],
2732 error_ok=True).strip()
2733 # Verify that the upstream branch has been uploaded too, otherwise
2734 # Gerrit will create additional CLs when uploading.
2735 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2736 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 DieWithError(
2738 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002739 'Note: maybe you\'ve uploaded it with --no-squash. '
2740 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 ' git cl upload --squash\n' % upstream_branch_name)
2742 else:
2743 parent = self.GetCommonAncestorWithUpstream()
2744
2745 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2746 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2747 '-m', message]).strip()
2748 else:
2749 change_desc = ChangeDescription(
2750 options.message or CreateDescriptionFromLog(args))
2751 if not change_desc.description:
2752 DieWithError("Description is empty. Aborting...")
2753
2754 if not git_footers.get_footer_change_id(change_desc.description):
2755 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002756 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2757 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002758 ref_to_push = 'HEAD'
2759 parent = '%s/%s' % (gerrit_remote, branch)
2760 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2761
2762 assert change_desc
2763 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2764 ref_to_push)]).splitlines()
2765 if len(commits) > 1:
2766 print('WARNING: This will upload %d commits. Run the following command '
2767 'to see which commits will be uploaded: ' % len(commits))
2768 print('git log %s..%s' % (parent, ref_to_push))
2769 print('You can also use `git squash-branch` to squash these into a '
2770 'single commit.')
2771 ask_for_data('About to upload; enter to confirm.')
2772
2773 if options.reviewers or options.tbr_owners:
2774 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2775 change)
2776
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002777 # Extra options that can be specified at push time. Doc:
2778 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2779 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002780 if change_desc.get_reviewers(tbr_only=True):
2781 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2782 refspec_opts.append('l=Code-Review+1')
2783
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002784 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002785 if not re.match(r'^[\w ]+$', options.title):
2786 options.title = re.sub(r'[^\w ]', '', options.title)
2787 print('WARNING: Patchset title may only contain alphanumeric chars '
2788 'and spaces. Cleaned up title:\n%s' % options.title)
2789 if not options.force:
2790 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002791 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2792 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002793 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2794
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002795 if options.send_mail:
2796 if not change_desc.get_reviewers():
2797 DieWithError('Must specify reviewers to send email.')
2798 refspec_opts.append('notify=ALL')
2799 else:
2800 refspec_opts.append('notify=NONE')
2801
tandrii99a72f22016-08-17 14:33:24 -07002802 reviewers = change_desc.get_reviewers()
2803 if reviewers:
2804 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002805
agablec6787972016-09-09 16:13:34 -07002806 if options.private:
2807 refspec_opts.append('draft')
2808
rmistry9eadede2016-09-19 11:22:43 -07002809 if options.topic:
2810 # Documentation on Gerrit topics is here:
2811 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2812 refspec_opts.append('topic=%s' % options.topic)
2813
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002814 refspec_suffix = ''
2815 if refspec_opts:
2816 refspec_suffix = '%' + ','.join(refspec_opts)
2817 assert ' ' not in refspec_suffix, (
2818 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002819 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002820
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002822 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823 print_stdout=True,
2824 # Flush after every line: useful for seeing progress when running as
2825 # recipe.
2826 filter_fn=lambda _: sys.stdout.flush())
2827
2828 if options.squash:
2829 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2830 change_numbers = [m.group(1)
2831 for m in map(regex.match, push_stdout.splitlines())
2832 if m]
2833 if len(change_numbers) != 1:
2834 DieWithError(
2835 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2836 'Change-Id: %s') % (len(change_numbers), change_id))
2837 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002838 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002839
2840 # Add cc's from the CC_LIST and --cc flag (if any).
2841 cc = self.GetCCList().split(',')
2842 if options.cc:
2843 cc.extend(options.cc)
2844 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002845 if change_desc.get_cced():
2846 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002847 if cc:
Aaron Gabledf86e302016-11-08 10:48:03 -08002848 errors = gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07002849 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
Aaron Gabledf86e302016-11-08 10:48:03 -08002850 if errors:
2851 return 1
tandrii88189772016-09-29 04:29:57 -07002852
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002853 return 0
2854
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002855 def _AddChangeIdToCommitMessage(self, options, args):
2856 """Re-commits using the current message, assumes the commit hook is in
2857 place.
2858 """
2859 log_desc = options.message or CreateDescriptionFromLog(args)
2860 git_command = ['commit', '--amend', '-m', log_desc]
2861 RunGit(git_command)
2862 new_log_desc = CreateDescriptionFromLog(args)
2863 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002864 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002865 return new_log_desc
2866 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002867 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002868
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002869 def SetCQState(self, new_state):
2870 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002871 vote_map = {
2872 _CQState.NONE: 0,
2873 _CQState.DRY_RUN: 1,
2874 _CQState.COMMIT : 2,
2875 }
2876 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2877 labels={'Commit-Queue': vote_map[new_state]})
2878
tandriie113dfd2016-10-11 10:20:12 -07002879 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002880 try:
2881 data = self._GetChangeDetail()
2882 except GerritIssueNotExists:
2883 return 'Gerrit doesn\'t know about your issue %s' % self.GetIssue()
2884
2885 if data['status'] in ('ABANDONED', 'MERGED'):
2886 return 'CL %s is closed' % self.GetIssue()
2887
2888 def GetTryjobProperties(self, patchset=None):
2889 """Returns dictionary of properties to launch tryjob."""
2890 data = self._GetChangeDetail(['ALL_REVISIONS'])
2891 patchset = int(patchset or self.GetPatchset())
2892 assert patchset
2893 revision_data = None # Pylint wants it to be defined.
2894 for revision_data in data['revisions'].itervalues():
2895 if int(revision_data['_number']) == patchset:
2896 break
2897 else:
2898 raise Exception('Patchset %d is not known in Gerrit issue %d' %
2899 (patchset, self.GetIssue()))
2900 return {
2901 'patch_issue': self.GetIssue(),
2902 'patch_set': patchset or self.GetPatchset(),
2903 'patch_project': data['project'],
2904 'patch_storage': 'gerrit',
2905 'patch_ref': revision_data['fetch']['http']['ref'],
2906 'patch_repository_url': revision_data['fetch']['http']['url'],
2907 'patch_gerrit_url': self.GetCodereviewServer(),
2908 }
tandriie113dfd2016-10-11 10:20:12 -07002909
tandriide281ae2016-10-12 06:02:30 -07002910 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002911 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002912
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002913
2914_CODEREVIEW_IMPLEMENTATIONS = {
2915 'rietveld': _RietveldChangelistImpl,
2916 'gerrit': _GerritChangelistImpl,
2917}
2918
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002919
iannuccie53c9352016-08-17 14:40:40 -07002920def _add_codereview_issue_select_options(parser, extra=""):
2921 _add_codereview_select_options(parser)
2922
2923 text = ('Operate on this issue number instead of the current branch\'s '
2924 'implicit issue.')
2925 if extra:
2926 text += ' '+extra
2927 parser.add_option('-i', '--issue', type=int, help=text)
2928
2929
2930def _process_codereview_issue_select_options(parser, options):
2931 _process_codereview_select_options(parser, options)
2932 if options.issue is not None and not options.forced_codereview:
2933 parser.error('--issue must be specified with either --rietveld or --gerrit')
2934
2935
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002936def _add_codereview_select_options(parser):
2937 """Appends --gerrit and --rietveld options to force specific codereview."""
2938 parser.codereview_group = optparse.OptionGroup(
2939 parser, 'EXPERIMENTAL! Codereview override options')
2940 parser.add_option_group(parser.codereview_group)
2941 parser.codereview_group.add_option(
2942 '--gerrit', action='store_true',
2943 help='Force the use of Gerrit for codereview')
2944 parser.codereview_group.add_option(
2945 '--rietveld', action='store_true',
2946 help='Force the use of Rietveld for codereview')
2947
2948
2949def _process_codereview_select_options(parser, options):
2950 if options.gerrit and options.rietveld:
2951 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2952 options.forced_codereview = None
2953 if options.gerrit:
2954 options.forced_codereview = 'gerrit'
2955 elif options.rietveld:
2956 options.forced_codereview = 'rietveld'
2957
2958
tandriif9aefb72016-07-01 09:06:51 -07002959def _get_bug_line_values(default_project, bugs):
2960 """Given default_project and comma separated list of bugs, yields bug line
2961 values.
2962
2963 Each bug can be either:
2964 * a number, which is combined with default_project
2965 * string, which is left as is.
2966
2967 This function may produce more than one line, because bugdroid expects one
2968 project per line.
2969
2970 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2971 ['v8:123', 'chromium:789']
2972 """
2973 default_bugs = []
2974 others = []
2975 for bug in bugs.split(','):
2976 bug = bug.strip()
2977 if bug:
2978 try:
2979 default_bugs.append(int(bug))
2980 except ValueError:
2981 others.append(bug)
2982
2983 if default_bugs:
2984 default_bugs = ','.join(map(str, default_bugs))
2985 if default_project:
2986 yield '%s:%s' % (default_project, default_bugs)
2987 else:
2988 yield default_bugs
2989 for other in sorted(others):
2990 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2991 yield other
2992
2993
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002994class ChangeDescription(object):
2995 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002996 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002997 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002998 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002999
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003000 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003002
agable@chromium.org42c20792013-09-12 17:34:49 +00003003 @property # www.logilab.org/ticket/89786
3004 def description(self): # pylint: disable=E0202
3005 return '\n'.join(self._description_lines)
3006
3007 def set_description(self, desc):
3008 if isinstance(desc, basestring):
3009 lines = desc.splitlines()
3010 else:
3011 lines = [line.rstrip() for line in desc]
3012 while lines and not lines[0]:
3013 lines.pop(0)
3014 while lines and not lines[-1]:
3015 lines.pop(-1)
3016 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003017
piman@chromium.org336f9122014-09-04 02:16:55 +00003018 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003019 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003021 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003022 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024
agable@chromium.org42c20792013-09-12 17:34:49 +00003025 # Get the set of R= and TBR= lines and remove them from the desciption.
3026 regexp = re.compile(self.R_LINE)
3027 matches = [regexp.match(line) for line in self._description_lines]
3028 new_desc = [l for i, l in enumerate(self._description_lines)
3029 if not matches[i]]
3030 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003031
agable@chromium.org42c20792013-09-12 17:34:49 +00003032 # Construct new unified R= and TBR= lines.
3033 r_names = []
3034 tbr_names = []
3035 for match in matches:
3036 if not match:
3037 continue
3038 people = cleanup_list([match.group(2).strip()])
3039 if match.group(1) == 'TBR':
3040 tbr_names.extend(people)
3041 else:
3042 r_names.extend(people)
3043 for name in r_names:
3044 if name not in reviewers:
3045 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003046 if add_owners_tbr:
3047 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003048 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003049 all_reviewers = set(tbr_names + reviewers)
3050 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3051 all_reviewers)
3052 tbr_names.extend(owners_db.reviewers_for(missing_files,
3053 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003054 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3055 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3056
3057 # Put the new lines in the description where the old first R= line was.
3058 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3059 if 0 <= line_loc < len(self._description_lines):
3060 if new_tbr_line:
3061 self._description_lines.insert(line_loc, new_tbr_line)
3062 if new_r_line:
3063 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003064 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003065 if new_r_line:
3066 self.append_footer(new_r_line)
3067 if new_tbr_line:
3068 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069
tandriif9aefb72016-07-01 09:06:51 -07003070 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003071 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003072 self.set_description([
3073 '# Enter a description of the change.',
3074 '# This will be displayed on the codereview site.',
3075 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003076 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003077 '--------------------',
3078 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 regexp = re.compile(self.BUG_LINE)
3081 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003082 prefix = settings.GetBugPrefix()
3083 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3084 for value in values:
3085 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3086 self.append_footer('BUG=%s' % value)
3087
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003089 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003090 if not content:
3091 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093
3094 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003095 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3096 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003097 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003099
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003100 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003101 """Adds a footer line to the description.
3102
3103 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3104 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3105 that Gerrit footers are always at the end.
3106 """
3107 parsed_footer_line = git_footers.parse_footer(line)
3108 if parsed_footer_line:
3109 # Line is a gerrit footer in the form: Footer-Key: any value.
3110 # Thus, must be appended observing Gerrit footer rules.
3111 self.set_description(
3112 git_footers.add_footer(self.description,
3113 key=parsed_footer_line[0],
3114 value=parsed_footer_line[1]))
3115 return
3116
3117 if not self._description_lines:
3118 self._description_lines.append(line)
3119 return
3120
3121 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3122 if gerrit_footers:
3123 # git_footers.split_footers ensures that there is an empty line before
3124 # actual (gerrit) footers, if any. We have to keep it that way.
3125 assert top_lines and top_lines[-1] == ''
3126 top_lines, separator = top_lines[:-1], top_lines[-1:]
3127 else:
3128 separator = [] # No need for separator if there are no gerrit_footers.
3129
3130 prev_line = top_lines[-1] if top_lines else ''
3131 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3132 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3133 top_lines.append('')
3134 top_lines.append(line)
3135 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003136
tandrii99a72f22016-08-17 14:33:24 -07003137 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003138 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003139 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003140 reviewers = [match.group(2).strip()
3141 for match in matches
3142 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003144
bradnelsond975b302016-10-23 12:20:23 -07003145 def get_cced(self):
3146 """Retrieves the list of reviewers."""
3147 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3148 cced = [match.group(2).strip() for match in matches if match]
3149 return cleanup_list(cced)
3150
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003151
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003152def get_approving_reviewers(props):
3153 """Retrieves the reviewers that approved a CL from the issue properties with
3154 messages.
3155
3156 Note that the list may contain reviewers that are not committer, thus are not
3157 considered by the CQ.
3158 """
3159 return sorted(
3160 set(
3161 message['sender']
3162 for message in props['messages']
3163 if message['approval'] and message['sender'] in props['reviewers']
3164 )
3165 )
3166
3167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168def FindCodereviewSettingsFile(filename='codereview.settings'):
3169 """Finds the given file starting in the cwd and going up.
3170
3171 Only looks up to the top of the repository unless an
3172 'inherit-review-settings-ok' file exists in the root of the repository.
3173 """
3174 inherit_ok_file = 'inherit-review-settings-ok'
3175 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003176 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003177 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3178 root = '/'
3179 while True:
3180 if filename in os.listdir(cwd):
3181 if os.path.isfile(os.path.join(cwd, filename)):
3182 return open(os.path.join(cwd, filename))
3183 if cwd == root:
3184 break
3185 cwd = os.path.dirname(cwd)
3186
3187
3188def LoadCodereviewSettingsFromFile(fileobj):
3189 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003190 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003192 def SetProperty(name, setting, unset_error_ok=False):
3193 fullname = 'rietveld.' + name
3194 if setting in keyvals:
3195 RunGit(['config', fullname, keyvals[setting]])
3196 else:
3197 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3198
tandrii48df5812016-10-17 03:55:37 -07003199 if not keyvals.get('GERRIT_HOST', False):
3200 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201 # Only server setting is required. Other settings can be absent.
3202 # In that case, we ignore errors raised during option deletion attempt.
3203 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003204 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3206 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003207 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003208 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003209 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3210 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003211 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003212 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003213 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003214 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003215 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3216 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003217
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003218 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003219 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003220
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003221 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003222 RunGit(['config', 'gerrit.squash-uploads',
3223 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003224
tandrii@chromium.org28253532016-04-14 13:46:56 +00003225 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003226 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003227 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3230 #should be of the form
3231 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3232 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3233 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3234 keyvals['ORIGIN_URL_CONFIG']])
3235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003236
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003237def urlretrieve(source, destination):
3238 """urllib is broken for SSL connections via a proxy therefore we
3239 can't use urllib.urlretrieve()."""
3240 with open(destination, 'w') as f:
3241 f.write(urllib2.urlopen(source).read())
3242
3243
ukai@chromium.org712d6102013-11-27 00:52:58 +00003244def hasSheBang(fname):
3245 """Checks fname is a #! script."""
3246 with open(fname) as f:
3247 return f.read(2).startswith('#!')
3248
3249
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003250# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3251def DownloadHooks(*args, **kwargs):
3252 pass
3253
3254
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003255def DownloadGerritHook(force):
3256 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003257
3258 Args:
3259 force: True to update hooks. False to install hooks if not present.
3260 """
3261 if not settings.GetIsGerrit():
3262 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003263 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003264 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3265 if not os.access(dst, os.X_OK):
3266 if os.path.exists(dst):
3267 if not force:
3268 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003269 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003270 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003271 if not hasSheBang(dst):
3272 DieWithError('Not a script: %s\n'
3273 'You need to download from\n%s\n'
3274 'into .git/hooks/commit-msg and '
3275 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003276 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3277 except Exception:
3278 if os.path.exists(dst):
3279 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003280 DieWithError('\nFailed to download hooks.\n'
3281 'You need to download from\n%s\n'
3282 'into .git/hooks/commit-msg and '
3283 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003284
3285
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003286
3287def GetRietveldCodereviewSettingsInteractively():
3288 """Prompt the user for settings."""
3289 server = settings.GetDefaultServerUrl(error_ok=True)
3290 prompt = 'Rietveld server (host[:port])'
3291 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3292 newserver = ask_for_data(prompt + ':')
3293 if not server and not newserver:
3294 newserver = DEFAULT_SERVER
3295 if newserver:
3296 newserver = gclient_utils.UpgradeToHttps(newserver)
3297 if newserver != server:
3298 RunGit(['config', 'rietveld.server', newserver])
3299
3300 def SetProperty(initial, caption, name, is_url):
3301 prompt = caption
3302 if initial:
3303 prompt += ' ("x" to clear) [%s]' % initial
3304 new_val = ask_for_data(prompt + ':')
3305 if new_val == 'x':
3306 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3307 elif new_val:
3308 if is_url:
3309 new_val = gclient_utils.UpgradeToHttps(new_val)
3310 if new_val != initial:
3311 RunGit(['config', 'rietveld.' + name, new_val])
3312
3313 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3314 SetProperty(settings.GetDefaultPrivateFlag(),
3315 'Private flag (rietveld only)', 'private', False)
3316 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3317 'tree-status-url', False)
3318 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3319 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3320 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3321 'run-post-upload-hook', False)
3322
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003323@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003324def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003325 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326
tandrii5d0a0422016-09-14 06:24:35 -07003327 print('WARNING: git cl config works for Rietveld only')
3328 # TODO(tandrii): remove this once we switch to Gerrit.
3329 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003330 parser.add_option('--activate-update', action='store_true',
3331 help='activate auto-updating [rietveld] section in '
3332 '.git/config')
3333 parser.add_option('--deactivate-update', action='store_true',
3334 help='deactivate auto-updating [rietveld] section in '
3335 '.git/config')
3336 options, args = parser.parse_args(args)
3337
3338 if options.deactivate_update:
3339 RunGit(['config', 'rietveld.autoupdate', 'false'])
3340 return
3341
3342 if options.activate_update:
3343 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3344 return
3345
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003346 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003347 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003348 return 0
3349
3350 url = args[0]
3351 if not url.endswith('codereview.settings'):
3352 url = os.path.join(url, 'codereview.settings')
3353
3354 # Load code review settings and download hooks (if available).
3355 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3356 return 0
3357
3358
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003359def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003360 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003361 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3362 branch = ShortBranchName(branchref)
3363 _, args = parser.parse_args(args)
3364 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003365 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003366 return RunGit(['config', 'branch.%s.base-url' % branch],
3367 error_ok=False).strip()
3368 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003369 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003370 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3371 error_ok=False).strip()
3372
3373
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003374def color_for_status(status):
3375 """Maps a Changelist status to color, for CMDstatus and other tools."""
3376 return {
3377 'unsent': Fore.RED,
3378 'waiting': Fore.BLUE,
3379 'reply': Fore.YELLOW,
3380 'lgtm': Fore.GREEN,
3381 'commit': Fore.MAGENTA,
3382 'closed': Fore.CYAN,
3383 'error': Fore.WHITE,
3384 }.get(status, Fore.WHITE)
3385
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003386
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003387def get_cl_statuses(changes, fine_grained, max_processes=None):
3388 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003389
3390 If fine_grained is true, this will fetch CL statuses from the server.
3391 Otherwise, simply indicate if there's a matching url for the given branches.
3392
3393 If max_processes is specified, it is used as the maximum number of processes
3394 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3395 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003396
3397 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003398 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003399 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003400 upload.verbosity = 0
3401
3402 if fine_grained:
3403 # Process one branch synchronously to work through authentication, then
3404 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003405 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003406 def fetch(cl):
3407 try:
3408 return (cl, cl.GetStatus())
3409 except:
3410 # See http://crbug.com/629863.
3411 logging.exception('failed to fetch status for %s:', cl)
3412 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003413 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003414
tandriiea9514a2016-08-17 12:32:37 -07003415 changes_to_fetch = changes[1:]
3416 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003417 # Exit early if there was only one branch to fetch.
3418 return
3419
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003420 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003421 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003422 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003423 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003424
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003425 fetched_cls = set()
3426 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003427 while True:
3428 try:
3429 row = it.next(timeout=5)
3430 except multiprocessing.TimeoutError:
3431 break
3432
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003433 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003434 yield row
3435
3436 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003437 for cl in set(changes_to_fetch) - fetched_cls:
3438 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003439
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003440 else:
3441 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003442 for cl in changes:
3443 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003444
rmistry@google.com2dd99862015-06-22 12:22:18 +00003445
3446def upload_branch_deps(cl, args):
3447 """Uploads CLs of local branches that are dependents of the current branch.
3448
3449 If the local branch dependency tree looks like:
3450 test1 -> test2.1 -> test3.1
3451 -> test3.2
3452 -> test2.2 -> test3.3
3453
3454 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3455 run on the dependent branches in this order:
3456 test2.1, test3.1, test3.2, test2.2, test3.3
3457
3458 Note: This function does not rebase your local dependent branches. Use it when
3459 you make a change to the parent branch that will not conflict with its
3460 dependent branches, and you would like their dependencies updated in
3461 Rietveld.
3462 """
3463 if git_common.is_dirty_git_tree('upload-branch-deps'):
3464 return 1
3465
3466 root_branch = cl.GetBranch()
3467 if root_branch is None:
3468 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3469 'Get on a branch!')
3470 if not cl.GetIssue() or not cl.GetPatchset():
3471 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3472 'patchset dependencies without an uploaded CL.')
3473
3474 branches = RunGit(['for-each-ref',
3475 '--format=%(refname:short) %(upstream:short)',
3476 'refs/heads'])
3477 if not branches:
3478 print('No local branches found.')
3479 return 0
3480
3481 # Create a dictionary of all local branches to the branches that are dependent
3482 # on it.
3483 tracked_to_dependents = collections.defaultdict(list)
3484 for b in branches.splitlines():
3485 tokens = b.split()
3486 if len(tokens) == 2:
3487 branch_name, tracked = tokens
3488 tracked_to_dependents[tracked].append(branch_name)
3489
vapiera7fbd5a2016-06-16 09:17:49 -07003490 print()
3491 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003492 dependents = []
3493 def traverse_dependents_preorder(branch, padding=''):
3494 dependents_to_process = tracked_to_dependents.get(branch, [])
3495 padding += ' '
3496 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003497 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003498 dependents.append(dependent)
3499 traverse_dependents_preorder(dependent, padding)
3500 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003501 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003502
3503 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003504 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003505 return 0
3506
vapiera7fbd5a2016-06-16 09:17:49 -07003507 print('This command will checkout all dependent branches and run '
3508 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003509 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3510
andybons@chromium.org962f9462016-02-03 20:00:42 +00003511 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003512 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003513 args.extend(['-t', 'Updated patchset dependency'])
3514
rmistry@google.com2dd99862015-06-22 12:22:18 +00003515 # Record all dependents that failed to upload.
3516 failures = {}
3517 # Go through all dependents, checkout the branch and upload.
3518 try:
3519 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003520 print()
3521 print('--------------------------------------')
3522 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003523 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003524 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003525 try:
3526 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003527 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003528 failures[dependent_branch] = 1
3529 except: # pylint: disable=W0702
3530 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003531 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003532 finally:
3533 # Swap back to the original root branch.
3534 RunGit(['checkout', '-q', root_branch])
3535
vapiera7fbd5a2016-06-16 09:17:49 -07003536 print()
3537 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003538 for dependent_branch in dependents:
3539 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print(' %s : %s' % (dependent_branch, upload_status))
3541 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003542
3543 return 0
3544
3545
kmarshall3bff56b2016-06-06 18:31:47 -07003546def CMDarchive(parser, args):
3547 """Archives and deletes branches associated with closed changelists."""
3548 parser.add_option(
3549 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003550 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003551 parser.add_option(
3552 '-f', '--force', action='store_true',
3553 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003554 parser.add_option(
3555 '-d', '--dry-run', action='store_true',
3556 help='Skip the branch tagging and removal steps.')
3557 parser.add_option(
3558 '-t', '--notags', action='store_true',
3559 help='Do not tag archived branches. '
3560 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003561
3562 auth.add_auth_options(parser)
3563 options, args = parser.parse_args(args)
3564 if args:
3565 parser.error('Unsupported args: %s' % ' '.join(args))
3566 auth_config = auth.extract_auth_config_from_options(options)
3567
3568 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3569 if not branches:
3570 return 0
3571
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003573 changes = [Changelist(branchref=b, auth_config=auth_config)
3574 for b in branches.splitlines()]
3575 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3576 statuses = get_cl_statuses(changes,
3577 fine_grained=True,
3578 max_processes=options.maxjobs)
3579 proposal = [(cl.GetBranch(),
3580 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3581 for cl, status in statuses
3582 if status == 'closed']
3583 proposal.sort()
3584
3585 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003587 return 0
3588
3589 current_branch = GetCurrentBranch()
3590
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003592 if options.notags:
3593 for next_item in proposal:
3594 print(' ' + next_item[0])
3595 else:
3596 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3597 for next_item in proposal:
3598 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003599
kmarshall9249e012016-08-23 12:02:16 -07003600 # Quit now on precondition failure or if instructed by the user, either
3601 # via an interactive prompt or by command line flags.
3602 if options.dry_run:
3603 print('\nNo changes were made (dry run).\n')
3604 return 0
3605 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003606 print('You are currently on a branch \'%s\' which is associated with a '
3607 'closed codereview issue, so archive cannot proceed. Please '
3608 'checkout another branch and run this command again.' %
3609 current_branch)
3610 return 1
kmarshall9249e012016-08-23 12:02:16 -07003611 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003612 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3613 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003615 return 1
3616
3617 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003618 if not options.notags:
3619 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003620 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003621
vapiera7fbd5a2016-06-16 09:17:49 -07003622 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003623
3624 return 0
3625
3626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003627def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003628 """Show status of changelists.
3629
3630 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003631 - Red not sent for review or broken
3632 - Blue waiting for review
3633 - Yellow waiting for you to reply to review
3634 - Green LGTM'ed
3635 - Magenta in the commit queue
3636 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003637
3638 Also see 'git cl comments'.
3639 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003641 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003642 parser.add_option('-f', '--fast', action='store_true',
3643 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003644 parser.add_option(
3645 '-j', '--maxjobs', action='store', type=int,
3646 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003647
3648 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003649 _add_codereview_issue_select_options(
3650 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003651 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003652 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003653 if args:
3654 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003655 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656
iannuccie53c9352016-08-17 14:40:40 -07003657 if options.issue is not None and not options.field:
3658 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003661 cl = Changelist(auth_config=auth_config, issue=options.issue,
3662 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 elif options.field == 'id':
3666 issueid = cl.GetIssue()
3667 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003668 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 elif options.field == 'patch':
3670 patchset = cl.GetPatchset()
3671 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003673 elif options.field == 'status':
3674 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675 elif options.field == 'url':
3676 url = cl.GetIssueURL()
3677 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003678 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003679 return 0
3680
3681 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3682 if not branches:
3683 print('No local branch found.')
3684 return 0
3685
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003686 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003687 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003688 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003689 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003690 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003691 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003692 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003693
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003694 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003695 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3696 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3697 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003698 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003699 c, status = output.next()
3700 branch_statuses[c.GetBranch()] = status
3701 status = branch_statuses.pop(branch)
3702 url = cl.GetIssueURL()
3703 if url and (not status or status == 'error'):
3704 # The issue probably doesn't exist anymore.
3705 url += ' (broken)'
3706
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003707 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003708 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003709 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003710 color = ''
3711 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003712 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003713 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003714 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003715 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003716
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003717 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print()
3719 print('Current branch:',)
3720 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003721 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003723 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003724 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003725 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print('Issue description:')
3727 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728 return 0
3729
3730
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003731def colorize_CMDstatus_doc():
3732 """To be called once in main() to add colors to git cl status help."""
3733 colors = [i for i in dir(Fore) if i[0].isupper()]
3734
3735 def colorize_line(line):
3736 for color in colors:
3737 if color in line.upper():
3738 # Extract whitespaces first and the leading '-'.
3739 indent = len(line) - len(line.lstrip(' ')) + 1
3740 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3741 return line
3742
3743 lines = CMDstatus.__doc__.splitlines()
3744 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3745
3746
phajdan.jre328cf92016-08-22 04:12:17 -07003747def write_json(path, contents):
3748 with open(path, 'w') as f:
3749 json.dump(contents, f)
3750
3751
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003752@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003754 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003755
3756 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003757 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003758 parser.add_option('-r', '--reverse', action='store_true',
3759 help='Lookup the branch(es) for the specified issues. If '
3760 'no issues are specified, all branches with mapped '
3761 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003762 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003763 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003764 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003765 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003766
dnj@chromium.org406c4402015-03-03 17:22:28 +00003767 if options.reverse:
3768 branches = RunGit(['for-each-ref', 'refs/heads',
3769 '--format=%(refname:short)']).splitlines()
3770
3771 # Reverse issue lookup.
3772 issue_branch_map = {}
3773 for branch in branches:
3774 cl = Changelist(branchref=branch)
3775 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3776 if not args:
3777 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003778 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003779 for issue in args:
3780 if not issue:
3781 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003782 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003783 print('Branch for issue number %s: %s' % (
3784 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003785 if options.json:
3786 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003787 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003788 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003789 if len(args) > 0:
3790 try:
3791 issue = int(args[0])
3792 except ValueError:
3793 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003794 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003795 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003796 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003797 if options.json:
3798 write_json(options.json, {
3799 'issue': cl.GetIssue(),
3800 'issue_url': cl.GetIssueURL(),
3801 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003802 return 0
3803
3804
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003805def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003806 """Shows or posts review comments for any changelist."""
3807 parser.add_option('-a', '--add-comment', dest='comment',
3808 help='comment to add to an issue')
3809 parser.add_option('-i', dest='issue',
3810 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003811 parser.add_option('-j', '--json-file',
3812 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003813 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003814 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003815 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003816
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003817 issue = None
3818 if options.issue:
3819 try:
3820 issue = int(options.issue)
3821 except ValueError:
3822 DieWithError('A review issue id is expected to be a number')
3823
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003824 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003825
3826 if options.comment:
3827 cl.AddComment(options.comment)
3828 return 0
3829
3830 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003831 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003832 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003833 summary.append({
3834 'date': message['date'],
3835 'lgtm': False,
3836 'message': message['text'],
3837 'not_lgtm': False,
3838 'sender': message['sender'],
3839 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003840 if message['disapproval']:
3841 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003842 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003843 elif message['approval']:
3844 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003845 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003846 elif message['sender'] == data['owner_email']:
3847 color = Fore.MAGENTA
3848 else:
3849 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003851 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003852 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003853 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003854 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003855 if options.json_file:
3856 with open(options.json_file, 'wb') as f:
3857 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003858 return 0
3859
3860
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003861@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003862def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003863 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003864 parser.add_option('-d', '--display', action='store_true',
3865 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003866 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003867 help='New description to set for this issue (- for stdin, '
3868 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003869 parser.add_option('-f', '--force', action='store_true',
3870 help='Delete any unpublished Gerrit edits for this issue '
3871 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003872
3873 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003874 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003875 options, args = parser.parse_args(args)
3876 _process_codereview_select_options(parser, options)
3877
3878 target_issue = None
3879 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003880 target_issue = ParseIssueNumberArgument(args[0])
3881 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003882 parser.print_help()
3883 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003884
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003885 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003886
martiniss6eda05f2016-06-30 10:18:35 -07003887 kwargs = {
3888 'auth_config': auth_config,
3889 'codereview': options.forced_codereview,
3890 }
3891 if target_issue:
3892 kwargs['issue'] = target_issue.issue
3893 if options.forced_codereview == 'rietveld':
3894 kwargs['rietveld_server'] = target_issue.hostname
3895
3896 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003897
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003898 if not cl.GetIssue():
3899 DieWithError('This branch has no associated changelist.')
3900 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003901
smut@google.com34fb6b12015-07-13 20:03:26 +00003902 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003903 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003904 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003905
3906 if options.new_description:
3907 text = options.new_description
3908 if text == '-':
3909 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003910 elif text == '+':
3911 base_branch = cl.GetCommonAncestorWithUpstream()
3912 change = cl.GetChange(base_branch, None, local_description=True)
3913 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003914
3915 description.set_description(text)
3916 else:
3917 description.prompt()
3918
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003919 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003920 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003921 return 0
3922
3923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924def CreateDescriptionFromLog(args):
3925 """Pulls out the commit log to use as a base for the CL description."""
3926 log_args = []
3927 if len(args) == 1 and not args[0].endswith('.'):
3928 log_args = [args[0] + '..']
3929 elif len(args) == 1 and args[0].endswith('...'):
3930 log_args = [args[0][:-1]]
3931 elif len(args) == 2:
3932 log_args = [args[0] + '..' + args[1]]
3933 else:
3934 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003935 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003936
3937
thestig@chromium.org44202a22014-03-11 19:22:18 +00003938def CMDlint(parser, args):
3939 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003940 parser.add_option('--filter', action='append', metavar='-x,+y',
3941 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003942 auth.add_auth_options(parser)
3943 options, args = parser.parse_args(args)
3944 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003945
3946 # Access to a protected member _XX of a client class
3947 # pylint: disable=W0212
3948 try:
3949 import cpplint
3950 import cpplint_chromium
3951 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003952 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003953 return 1
3954
3955 # Change the current working directory before calling lint so that it
3956 # shows the correct base.
3957 previous_cwd = os.getcwd()
3958 os.chdir(settings.GetRoot())
3959 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003960 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003961 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3962 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003963 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003965 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003966
3967 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003968 command = args + files
3969 if options.filter:
3970 command = ['--filter=' + ','.join(options.filter)] + command
3971 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003972
3973 white_regex = re.compile(settings.GetLintRegex())
3974 black_regex = re.compile(settings.GetLintIgnoreRegex())
3975 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3976 for filename in filenames:
3977 if white_regex.match(filename):
3978 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003979 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003980 else:
3981 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3982 extra_check_functions)
3983 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003984 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003985 finally:
3986 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003987 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003988 if cpplint._cpplint_state.error_count != 0:
3989 return 1
3990 return 0
3991
3992
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003994 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003995 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003997 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003998 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003999 auth.add_auth_options(parser)
4000 options, args = parser.parse_args(args)
4001 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004002
sbc@chromium.org71437c02015-04-09 19:29:40 +00004003 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004004 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 return 1
4006
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004007 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004008 if args:
4009 base_branch = args[0]
4010 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004011 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004012 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004014 cl.RunHook(
4015 committing=not options.upload,
4016 may_prompt=False,
4017 verbose=options.verbose,
4018 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004019 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004020
4021
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004022def GenerateGerritChangeId(message):
4023 """Returns Ixxxxxx...xxx change id.
4024
4025 Works the same way as
4026 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4027 but can be called on demand on all platforms.
4028
4029 The basic idea is to generate git hash of a state of the tree, original commit
4030 message, author/committer info and timestamps.
4031 """
4032 lines = []
4033 tree_hash = RunGitSilent(['write-tree'])
4034 lines.append('tree %s' % tree_hash.strip())
4035 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4036 if code == 0:
4037 lines.append('parent %s' % parent.strip())
4038 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4039 lines.append('author %s' % author.strip())
4040 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4041 lines.append('committer %s' % committer.strip())
4042 lines.append('')
4043 # Note: Gerrit's commit-hook actually cleans message of some lines and
4044 # whitespace. This code is not doing this, but it clearly won't decrease
4045 # entropy.
4046 lines.append(message)
4047 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4048 stdin='\n'.join(lines))
4049 return 'I%s' % change_hash.strip()
4050
4051
wittman@chromium.org455dc922015-01-26 20:15:50 +00004052def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4053 """Computes the remote branch ref to use for the CL.
4054
4055 Args:
4056 remote (str): The git remote for the CL.
4057 remote_branch (str): The git remote branch for the CL.
4058 target_branch (str): The target branch specified by the user.
4059 pending_prefix (str): The pending prefix from the settings.
4060 """
4061 if not (remote and remote_branch):
4062 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004063
wittman@chromium.org455dc922015-01-26 20:15:50 +00004064 if target_branch:
4065 # Cannonicalize branch references to the equivalent local full symbolic
4066 # refs, which are then translated into the remote full symbolic refs
4067 # below.
4068 if '/' not in target_branch:
4069 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4070 else:
4071 prefix_replacements = (
4072 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4073 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4074 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4075 )
4076 match = None
4077 for regex, replacement in prefix_replacements:
4078 match = re.search(regex, target_branch)
4079 if match:
4080 remote_branch = target_branch.replace(match.group(0), replacement)
4081 break
4082 if not match:
4083 # This is a branch path but not one we recognize; use as-is.
4084 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004085 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4086 # Handle the refs that need to land in different refs.
4087 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004088
wittman@chromium.org455dc922015-01-26 20:15:50 +00004089 # Create the true path to the remote branch.
4090 # Does the following translation:
4091 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4092 # * refs/remotes/origin/master -> refs/heads/master
4093 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4094 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4095 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4096 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4097 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4098 'refs/heads/')
4099 elif remote_branch.startswith('refs/remotes/branch-heads'):
4100 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4101 # If a pending prefix exists then replace refs/ with it.
4102 if pending_prefix:
4103 remote_branch = remote_branch.replace('refs/', pending_prefix)
4104 return remote_branch
4105
4106
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004107def cleanup_list(l):
4108 """Fixes a list so that comma separated items are put as individual items.
4109
4110 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4111 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4112 """
4113 items = sum((i.split(',') for i in l), [])
4114 stripped_items = (i.strip() for i in items)
4115 return sorted(filter(None, stripped_items))
4116
4117
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004118@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004119def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004120 """Uploads the current changelist to codereview.
4121
4122 Can skip dependency patchset uploads for a branch by running:
4123 git config branch.branch_name.skip-deps-uploads True
4124 To unset run:
4125 git config --unset branch.branch_name.skip-deps-uploads
4126 Can also set the above globally by using the --global flag.
4127 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004128 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4129 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004130 parser.add_option('--bypass-watchlists', action='store_true',
4131 dest='bypass_watchlists',
4132 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004133 parser.add_option('-f', action='store_true', dest='force',
4134 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004135 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004136 parser.add_option('-b', '--bug',
4137 help='pre-populate the bug number(s) for this issue. '
4138 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004139 parser.add_option('--message-file', dest='message_file',
4140 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004141 parser.add_option('-t', dest='title',
4142 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004143 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004144 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004145 help='reviewer email addresses')
4146 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004147 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004148 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004149 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004150 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004151 parser.add_option('--emulate_svn_auto_props',
4152 '--emulate-svn-auto-props',
4153 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004154 dest="emulate_svn_auto_props",
4155 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004156 parser.add_option('-c', '--use-commit-queue', action='store_true',
4157 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004158 parser.add_option('--private', action='store_true',
4159 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004160 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004161 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004162 metavar='TARGET',
4163 help='Apply CL to remote ref TARGET. ' +
4164 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004165 parser.add_option('--squash', action='store_true',
4166 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004167 parser.add_option('--no-squash', action='store_true',
4168 help='Don\'t squash multiple commits into one ' +
4169 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004170 parser.add_option('--topic', default=None,
4171 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004172 parser.add_option('--email', default=None,
4173 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004174 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4175 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004176 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4177 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004178 help='Send the patchset to do a CQ dry run right after '
4179 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004180 parser.add_option('--dependencies', action='store_true',
4181 help='Uploads CLs of all the local branches that depend on '
4182 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004183
rmistry@google.com2dd99862015-06-22 12:22:18 +00004184 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004185 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004186 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004187 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004188 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004189 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004190 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004191
sbc@chromium.org71437c02015-04-09 19:29:40 +00004192 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004193 return 1
4194
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004195 options.reviewers = cleanup_list(options.reviewers)
4196 options.cc = cleanup_list(options.cc)
4197
tandriib80458a2016-06-23 12:20:07 -07004198 if options.message_file:
4199 if options.message:
4200 parser.error('only one of --message and --message-file allowed.')
4201 options.message = gclient_utils.FileRead(options.message_file)
4202 options.message_file = None
4203
tandrii4d0545a2016-07-06 03:56:49 -07004204 if options.cq_dry_run and options.use_commit_queue:
4205 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4206
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004207 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4208 settings.GetIsGerrit()
4209
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004210 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004211 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004212
4213
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004214def IsSubmoduleMergeCommit(ref):
4215 # When submodules are added to the repo, we expect there to be a single
4216 # non-git-svn merge commit at remote HEAD with a signature comment.
4217 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004218 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004219 return RunGit(cmd) != ''
4220
4221
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004223 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004225 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4226 upstream and closes the issue automatically and atomically.
4227
4228 Otherwise (in case of Rietveld):
4229 Squashes branch into a single commit.
4230 Updates changelog with metadata (e.g. pointer to review).
4231 Pushes/dcommits the code upstream.
4232 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233 """
4234 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4235 help='bypass upload presubmit hook')
4236 parser.add_option('-m', dest='message',
4237 help="override review description")
4238 parser.add_option('-f', action='store_true', dest='force',
4239 help="force yes to questions (don't prompt)")
4240 parser.add_option('-c', dest='contributor',
4241 help="external contributor for patch (appended to " +
4242 "description and used as author for git). Should be " +
4243 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004244 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004245 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004247 auth_config = auth.extract_auth_config_from_options(options)
4248
4249 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004251 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4252 if cl.IsGerrit():
4253 if options.message:
4254 # This could be implemented, but it requires sending a new patch to
4255 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4256 # Besides, Gerrit has the ability to change the commit message on submit
4257 # automatically, thus there is no need to support this option (so far?).
4258 parser.error('-m MESSAGE option is not supported for Gerrit.')
4259 if options.contributor:
4260 parser.error(
4261 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4262 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4263 'the contributor\'s "name <email>". If you can\'t upload such a '
4264 'commit for review, contact your repository admin and request'
4265 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004266 if not cl.GetIssue():
4267 DieWithError('You must upload the issue first to Gerrit.\n'
4268 ' If you would rather have `git cl land` upload '
4269 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004270 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4271 options.verbose)
4272
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004273 current = cl.GetBranch()
4274 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4275 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004276 print()
4277 print('Attempting to push branch %r into another local branch!' % current)
4278 print()
4279 print('Either reparent this branch on top of origin/master:')
4280 print(' git reparent-branch --root')
4281 print()
4282 print('OR run `git rebase-update` if you think the parent branch is ')
4283 print('already committed.')
4284 print()
4285 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004286 return 1
4287
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004288 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004289 # Default to merging against our best guess of the upstream branch.
4290 args = [cl.GetUpstreamBranch()]
4291
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004292 if options.contributor:
4293 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004294 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004295 return 1
4296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004297 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004298 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299
sbc@chromium.org71437c02015-04-09 19:29:40 +00004300 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301 return 1
4302
4303 # This rev-list syntax means "show all commits not in my branch that
4304 # are in base_branch".
4305 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4306 base_branch]).splitlines()
4307 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004308 print('Base branch "%s" has %d commits '
4309 'not in this branch.' % (base_branch, len(upstream_commits)))
4310 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311 return 1
4312
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004313 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004314 svn_head = None
4315 if cmd == 'dcommit' or base_has_submodules:
4316 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4317 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004318
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004319 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004320 # If the base_head is a submodule merge commit, the first parent of the
4321 # base_head should be a git-svn commit, which is what we're interested in.
4322 base_svn_head = base_branch
4323 if base_has_submodules:
4324 base_svn_head += '^1'
4325
4326 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004327 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print('This branch has %d additional commits not upstreamed yet.'
4329 % len(extra_commits.splitlines()))
4330 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4331 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 return 1
4333
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004334 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004335 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004336 author = None
4337 if options.contributor:
4338 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004339 hook_results = cl.RunHook(
4340 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004341 may_prompt=not options.force,
4342 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004343 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004344 if not hook_results.should_continue():
4345 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004346
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004347 # Check the tree status if the tree status URL is set.
4348 status = GetTreeStatus()
4349 if 'closed' == status:
4350 print('The tree is closed. Please wait for it to reopen. Use '
4351 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4352 return 1
4353 elif 'unknown' == status:
4354 print('Unable to determine tree status. Please verify manually and '
4355 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4356 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004358 change_desc = ChangeDescription(options.message)
4359 if not change_desc.description and cl.GetIssue():
4360 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004361
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004362 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004363 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004364 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004365 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004366 print('No description set.')
4367 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004368 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004370 # Keep a separate copy for the commit message, because the commit message
4371 # contains the link to the Rietveld issue, while the Rietveld message contains
4372 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004373 # Keep a separate copy for the commit message.
4374 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004375 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004376
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004377 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004378 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004379 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004380 # after it. Add a period on a new line to circumvent this. Also add a space
4381 # before the period to make sure that Gitiles continues to correctly resolve
4382 # the URL.
4383 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004385 commit_desc.append_footer('Patch from %s.' % options.contributor)
4386
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004387 print('Description:')
4388 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004390 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004392 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004393
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004394 # We want to squash all this branch's commits into one commit with the proper
4395 # description. We do this by doing a "reset --soft" to the base branch (which
4396 # keeps the working copy the same), then dcommitting that. If origin/master
4397 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4398 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004400 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4401 # Delete the branches if they exist.
4402 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4403 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4404 result = RunGitWithCode(showref_cmd)
4405 if result[0] == 0:
4406 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407
4408 # We might be in a directory that's present in this branch but not in the
4409 # trunk. Move up to the top of the tree so that git commands that expect a
4410 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004411 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412 if rel_base_path:
4413 os.chdir(rel_base_path)
4414
4415 # Stuff our change into the merge branch.
4416 # We wrap in a try...finally block so if anything goes wrong,
4417 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004418 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004419 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004420 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004421 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004422 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004423 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004424 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004426 RunGit(
4427 [
4428 'commit', '--author', options.contributor,
4429 '-m', commit_desc.description,
4430 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004432 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004433 if base_has_submodules:
4434 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4435 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4436 RunGit(['checkout', CHERRY_PICK_BRANCH])
4437 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004438 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004439 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004440 mirror = settings.GetGitMirror(remote)
4441 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004442 pending_prefix = settings.GetPendingRefPrefix()
4443 if not pending_prefix or branch.startswith(pending_prefix):
4444 # If not using refs/pending/heads/* at all, or target ref is already set
4445 # to pending, then push to the target ref directly.
4446 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004447 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004448 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004449 else:
4450 # Cherry-pick the change on top of pending ref and then push it.
4451 assert branch.startswith('refs/'), branch
4452 assert pending_prefix[-1] == '/', pending_prefix
4453 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004454 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004455 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004456 if retcode == 0:
4457 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004458 else:
4459 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004460 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004461 'svn', 'dcommit',
4462 '-C%s' % options.similarity,
4463 '--no-rebase', '--rmdir',
4464 ]
4465 if settings.GetForceHttpsCommitUrl():
4466 # Allow forcing https commit URLs for some projects that don't allow
4467 # committing to http URLs (like Google Code).
4468 remote_url = cl.GetGitSvnRemoteUrl()
4469 if urlparse.urlparse(remote_url).scheme == 'http':
4470 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004471 cmd_args.append('--commit-url=%s' % remote_url)
4472 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004473 if 'Committed r' in output:
4474 revision = re.match(
4475 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4476 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004477 finally:
4478 # And then swap back to the original branch and clean up.
4479 RunGit(['checkout', '-q', cl.GetBranch()])
4480 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004481 if base_has_submodules:
4482 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004483
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004484 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004485 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004486 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004487
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004488 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004489 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004490 try:
4491 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4492 # We set pushed_to_pending to False, since it made it all the way to the
4493 # real ref.
4494 pushed_to_pending = False
4495 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004496 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004497
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004498 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004499 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004500 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004501 if not to_pending:
4502 if viewvc_url and revision:
4503 change_desc.append_footer(
4504 'Committed: %s%s' % (viewvc_url, revision))
4505 elif revision:
4506 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print('Closing issue '
4508 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004509 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004510 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004511 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004512 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004513 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004514 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004515 if options.bypass_hooks:
4516 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4517 else:
4518 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004519 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004520
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004521 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004522 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004523 print('The commit is in the pending queue (%s).' % pending_ref)
4524 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4525 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004526
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004527 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4528 if os.path.isfile(hook):
4529 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004530
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004531 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532
4533
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004534def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004535 print()
4536 print('Waiting for commit to be landed on %s...' % real_ref)
4537 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004538 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4539 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004540 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004541
4542 loop = 0
4543 while True:
4544 sys.stdout.write('fetching (%d)... \r' % loop)
4545 sys.stdout.flush()
4546 loop += 1
4547
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004548 if mirror:
4549 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004550 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4551 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4552 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4553 for commit in commits.splitlines():
4554 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004555 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004556 return commit
4557
4558 current_rev = to_rev
4559
4560
tandriibf429402016-09-14 07:09:12 -07004561def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004562 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4563
4564 Returns:
4565 (retcode of last operation, output log of last operation).
4566 """
4567 assert pending_ref.startswith('refs/'), pending_ref
4568 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4569 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4570 code = 0
4571 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004572 max_attempts = 3
4573 attempts_left = max_attempts
4574 while attempts_left:
4575 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004577 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004578
4579 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004580 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004581 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004582 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004583 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004584 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004585 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004586 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004587 continue
4588
4589 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004590 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004591 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004592 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004593 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004594 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4595 'the following files have merge conflicts:' % pending_ref)
4596 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4597 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004598 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004599 return code, out
4600
4601 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004602 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004603 code, out = RunGitWithCode(
4604 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4605 if code == 0:
4606 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004607 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004608 return code, out
4609
vapiera7fbd5a2016-06-16 09:17:49 -07004610 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004611 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004612 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004613 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004614 print('Fatal push error. Make sure your .netrc credentials and git '
4615 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004616 return code, out
4617
vapiera7fbd5a2016-06-16 09:17:49 -07004618 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004619 return code, out
4620
4621
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004622def IsFatalPushFailure(push_stdout):
4623 """True if retrying push won't help."""
4624 return '(prohibited by Gerrit)' in push_stdout
4625
4626
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004627@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004629 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004630 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004631 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004632 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004633 message = """This repository appears to be a git-svn mirror, but we
4634don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004635 else:
4636 message = """This doesn't appear to be an SVN repository.
4637If your project has a true, writeable git repository, you probably want to run
4638'git cl land' instead.
4639If your project has a git mirror of an upstream SVN master, you probably need
4640to run 'git svn init'.
4641
4642Using the wrong command might cause your commit to appear to succeed, and the
4643review to be closed, without actually landing upstream. If you choose to
4644proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004645 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004646 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004647 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4648 'Please let us know of this project you are committing to:'
4649 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650 return SendUpstream(parser, args, 'dcommit')
4651
4652
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004653@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004654def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004655 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004656 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004657 print('This appears to be an SVN repository.')
4658 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004659 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004660 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004661 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662
4663
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004664@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004666 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004667 parser.add_option('-b', dest='newbranch',
4668 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004669 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004670 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004671 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4672 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004673 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004674 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004675 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004676 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004677 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004678 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004679
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004680
4681 group = optparse.OptionGroup(
4682 parser,
4683 'Options for continuing work on the current issue uploaded from a '
4684 'different clone (e.g. different machine). Must be used independently '
4685 'from the other options. No issue number should be specified, and the '
4686 'branch must have an issue number associated with it')
4687 group.add_option('--reapply', action='store_true', dest='reapply',
4688 help='Reset the branch and reapply the issue.\n'
4689 'CAUTION: This will undo any local changes in this '
4690 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004691
4692 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004693 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004694 parser.add_option_group(group)
4695
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004697 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004699 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004700 auth_config = auth.extract_auth_config_from_options(options)
4701
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004702
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004703 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004704 if options.newbranch:
4705 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004706 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004707 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004708
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004709 cl = Changelist(auth_config=auth_config,
4710 codereview=options.forced_codereview)
4711 if not cl.GetIssue():
4712 parser.error('current branch must have an associated issue')
4713
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004714 upstream = cl.GetUpstreamBranch()
4715 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004716 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004717
4718 RunGit(['reset', '--hard', upstream])
4719 if options.pull:
4720 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004721
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004722 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4723 options.directory)
4724
4725 if len(args) != 1 or not args[0]:
4726 parser.error('Must specify issue number or url')
4727
4728 # We don't want uncommitted changes mixed up with the patch.
4729 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004730 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004731
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004732 if options.newbranch:
4733 if options.force:
4734 RunGit(['branch', '-D', options.newbranch],
4735 stderr=subprocess2.PIPE, error_ok=True)
4736 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004737 elif not GetCurrentBranch():
4738 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004739
4740 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4741
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004742 if cl.IsGerrit():
4743 if options.reject:
4744 parser.error('--reject is not supported with Gerrit codereview.')
4745 if options.nocommit:
4746 parser.error('--nocommit is not supported with Gerrit codereview.')
4747 if options.directory:
4748 parser.error('--directory is not supported with Gerrit codereview.')
4749
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004750 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004751 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004752
4753
4754def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004755 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756 # Provide a wrapper for git svn rebase to help avoid accidental
4757 # git svn dcommit.
4758 # It's the only command that doesn't use parser at all since we just defer
4759 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004760
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004761 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004762
4763
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004764def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004765 """Fetches the tree status and returns either 'open', 'closed',
4766 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004767 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004768 if url:
4769 status = urllib2.urlopen(url).read().lower()
4770 if status.find('closed') != -1 or status == '0':
4771 return 'closed'
4772 elif status.find('open') != -1 or status == '1':
4773 return 'open'
4774 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004775 return 'unset'
4776
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004778def GetTreeStatusReason():
4779 """Fetches the tree status from a json url and returns the message
4780 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004781 url = settings.GetTreeStatusUrl()
4782 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783 connection = urllib2.urlopen(json_url)
4784 status = json.loads(connection.read())
4785 connection.close()
4786 return status['message']
4787
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004790 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004791 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004792 status = GetTreeStatus()
4793 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004794 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004795 return 2
4796
vapiera7fbd5a2016-06-16 09:17:49 -07004797 print('The tree is %s' % status)
4798 print()
4799 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004800 if status != 'open':
4801 return 1
4802 return 0
4803
4804
maruel@chromium.org15192402012-09-06 12:38:29 +00004805def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004806 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004807 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004808 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004809 '-b', '--bot', action='append',
4810 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4811 'times to specify multiple builders. ex: '
4812 '"-b win_rel -b win_layout". See '
4813 'the try server waterfall for the builders name and the tests '
4814 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004815 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004816 '-B', '--bucket', default='',
4817 help=('Buildbucket bucket to send the try requests.'))
4818 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004819 '-m', '--master', default='',
4820 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004821 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004822 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004823 help='Revision to use for the try job; default: the revision will '
4824 'be determined by the try recipe that builder runs, which usually '
4825 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004828 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004829 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004830 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004831 '--project',
4832 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004833 'in recipe to determine to which repository or directory to '
4834 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004835 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004836 '-p', '--property', dest='properties', action='append', default=[],
4837 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004838 'key2=value2 etc. The value will be treated as '
4839 'json if decodable, or as string otherwise. '
4840 'NOTE: using this may make your try job not usable for CQ, '
4841 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004842 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004843 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4844 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004845 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004846 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004847 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004848 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004849
machenbach@chromium.org45453142015-09-15 08:45:22 +00004850 # Make sure that all properties are prop=value pairs.
4851 bad_params = [x for x in options.properties if '=' not in x]
4852 if bad_params:
4853 parser.error('Got properties with missing "=": %s' % bad_params)
4854
maruel@chromium.org15192402012-09-06 12:38:29 +00004855 if args:
4856 parser.error('Unknown arguments: %s' % args)
4857
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004858 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004859 if not cl.GetIssue():
4860 parser.error('Need to upload first')
4861
tandriie113dfd2016-10-11 10:20:12 -07004862 error_message = cl.CannotTriggerTryJobReason()
4863 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004864 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004865
borenet6c0efe62016-10-19 08:13:29 -07004866 if options.bucket and options.master:
4867 parser.error('Only one of --bucket and --master may be used.')
4868
qyearsley1fdfcb62016-10-24 13:22:03 -07004869 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004870
qyearsleydd49f942016-10-28 11:57:22 -07004871 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4872 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004873 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004874 if options.verbose:
4875 print('git cl try with no bots now defaults to CQ Dry Run.')
4876 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004877
borenet6c0efe62016-10-19 08:13:29 -07004878 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004879 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004880 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004881 'of bot requires an initial job from a parent (usually a builder). '
4882 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004883 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004884 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004885
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004886 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004887 if patchset != cl.GetPatchset():
4888 print('Warning: Codereview server has newer patchsets (%s) than most '
4889 'recent upload from local checkout (%s). Did a previous upload '
4890 'fail?\n'
4891 'By default, git cl try uses the latest patchset from '
4892 'codereview, continuing to use patchset %s.\n' %
4893 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004894
tandrii568043b2016-10-11 07:49:18 -07004895 try:
borenet6c0efe62016-10-19 08:13:29 -07004896 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4897 patchset)
tandrii568043b2016-10-11 07:49:18 -07004898 except BuildbucketResponseException as ex:
4899 print('ERROR: %s' % ex)
4900 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004901 return 0
4902
4903
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004904def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004905 """Prints info about try jobs associated with current CL."""
4906 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004907 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004908 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004909 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004910 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004911 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004912 '--color', action='store_true', default=setup_color.IS_TTY,
4913 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004914 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004915 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4916 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004917 group.add_option(
4918 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004919 parser.add_option_group(group)
4920 auth.add_auth_options(parser)
4921 options, args = parser.parse_args(args)
4922 if args:
4923 parser.error('Unrecognized args: %s' % ' '.join(args))
4924
4925 auth_config = auth.extract_auth_config_from_options(options)
4926 cl = Changelist(auth_config=auth_config)
4927 if not cl.GetIssue():
4928 parser.error('Need to upload first')
4929
tandrii221ab252016-10-06 08:12:04 -07004930 patchset = options.patchset
4931 if not patchset:
4932 patchset = cl.GetMostRecentPatchset()
4933 if not patchset:
4934 parser.error('Codereview doesn\'t know about issue %s. '
4935 'No access to issue or wrong issue number?\n'
4936 'Either upload first, or pass --patchset explicitely' %
4937 cl.GetIssue())
4938
4939 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004940 print('Warning: Codereview server has newer patchsets (%s) than most '
4941 'recent upload from local checkout (%s). Did a previous upload '
4942 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004943 'By default, git cl try-results uses the latest patchset from '
4944 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004945 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004946 try:
tandrii221ab252016-10-06 08:12:04 -07004947 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004948 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004949 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004950 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004951 if options.json:
4952 write_try_results_json(options.json, jobs)
4953 else:
4954 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004955 return 0
4956
4957
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004958@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004959def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004960 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004961 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004962 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004963 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004965 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004966 if args:
4967 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004968 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004969 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004970 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004971 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004972
4973 # Clear configured merge-base, if there is one.
4974 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004975 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004976 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004977 return 0
4978
4979
thestig@chromium.org00858c82013-12-02 23:08:03 +00004980def CMDweb(parser, args):
4981 """Opens the current CL in the web browser."""
4982 _, args = parser.parse_args(args)
4983 if args:
4984 parser.error('Unrecognized args: %s' % ' '.join(args))
4985
4986 issue_url = Changelist().GetIssueURL()
4987 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004988 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004989 return 1
4990
4991 webbrowser.open(issue_url)
4992 return 0
4993
4994
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004995def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004996 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004997 parser.add_option('-d', '--dry-run', action='store_true',
4998 help='trigger in dry run mode')
4999 parser.add_option('-c', '--clear', action='store_true',
5000 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005001 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005002 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005003 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005004 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005005 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005006 if args:
5007 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005008 if options.dry_run and options.clear:
5009 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5010
iannuccie53c9352016-08-17 14:40:40 -07005011 cl = Changelist(auth_config=auth_config, issue=options.issue,
5012 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005013 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005014 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005015 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005016 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005017 state = _CQState.DRY_RUN
5018 else:
5019 state = _CQState.COMMIT
5020 if not cl.GetIssue():
5021 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005022 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005023 return 0
5024
5025
groby@chromium.org411034a2013-02-26 15:12:01 +00005026def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005027 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005028 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005029 auth.add_auth_options(parser)
5030 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005031 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005032 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005033 if args:
5034 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005035 cl = Changelist(auth_config=auth_config, issue=options.issue,
5036 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005037 # Ensure there actually is an issue to close.
5038 cl.GetDescription()
5039 cl.CloseIssue()
5040 return 0
5041
5042
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005043def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005044 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005045 parser.add_option(
5046 '--stat',
5047 action='store_true',
5048 dest='stat',
5049 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005050 auth.add_auth_options(parser)
5051 options, args = parser.parse_args(args)
5052 auth_config = auth.extract_auth_config_from_options(options)
5053 if args:
5054 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005055
5056 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005057 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005058 # Staged changes would be committed along with the patch from last
5059 # upload, hence counted toward the "last upload" side in the final
5060 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005061 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005062 return 1
5063
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005064 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005065 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005066 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005067 if not issue:
5068 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005069 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005070 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005071
5072 # Create a new branch based on the merge-base
5073 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005074 # Clear cached branch in cl object, to avoid overwriting original CL branch
5075 # properties.
5076 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005077 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005078 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005079 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005080 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005081 return rtn
5082
wychen@chromium.org06928532015-02-03 02:11:29 +00005083 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005084 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005085 cmd = ['git', 'diff']
5086 if options.stat:
5087 cmd.append('--stat')
5088 cmd.extend([TMP_BRANCH, branch, '--'])
5089 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005090 finally:
5091 RunGit(['checkout', '-q', branch])
5092 RunGit(['branch', '-D', TMP_BRANCH])
5093
5094 return 0
5095
5096
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005097def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005098 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005099 parser.add_option(
5100 '--no-color',
5101 action='store_true',
5102 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005103 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005104 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005105 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005106
5107 author = RunGit(['config', 'user.email']).strip() or None
5108
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005109 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005110
5111 if args:
5112 if len(args) > 1:
5113 parser.error('Unknown args')
5114 base_branch = args[0]
5115 else:
5116 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005117 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005118
5119 change = cl.GetChange(base_branch, None)
5120 return owners_finder.OwnersFinder(
5121 [f.LocalPath() for f in
5122 cl.GetChange(base_branch, None).AffectedFiles()],
5123 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005124 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005125 disable_color=options.no_color).run()
5126
5127
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005129 """Generates a diff command."""
5130 # Generate diff for the current branch's changes.
5131 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5132 upstream_commit, '--' ]
5133
5134 if args:
5135 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005136 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005137 diff_cmd.append(arg)
5138 else:
5139 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005140
5141 return diff_cmd
5142
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005143def MatchingFileType(file_name, extensions):
5144 """Returns true if the file name ends with one of the given extensions."""
5145 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005146
enne@chromium.org555cfe42014-01-29 18:21:39 +00005147@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005148def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005149 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005150 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005151 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005152 parser.add_option('--full', action='store_true',
5153 help='Reformat the full content of all touched files')
5154 parser.add_option('--dry-run', action='store_true',
5155 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005156 parser.add_option('--python', action='store_true',
5157 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005158 parser.add_option('--diff', action='store_true',
5159 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005160 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005161
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005162 # git diff generates paths against the root of the repository. Change
5163 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005164 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005165 if rel_base_path:
5166 os.chdir(rel_base_path)
5167
digit@chromium.org29e47272013-05-17 17:01:46 +00005168 # Grab the merge-base commit, i.e. the upstream commit of the current
5169 # branch when it was created or the last time it was rebased. This is
5170 # to cover the case where the user may have called "git fetch origin",
5171 # moving the origin branch to a newer commit, but hasn't rebased yet.
5172 upstream_commit = None
5173 cl = Changelist()
5174 upstream_branch = cl.GetUpstreamBranch()
5175 if upstream_branch:
5176 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5177 upstream_commit = upstream_commit.strip()
5178
5179 if not upstream_commit:
5180 DieWithError('Could not find base commit for this branch. '
5181 'Are you in detached state?')
5182
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005183 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5184 diff_output = RunGit(changed_files_cmd)
5185 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005186 # Filter out files deleted by this CL
5187 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005188
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005189 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5190 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5191 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005192 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005193
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005194 top_dir = os.path.normpath(
5195 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5196
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005197 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5198 # formatted. This is used to block during the presubmit.
5199 return_value = 0
5200
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005201 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005202 # Locate the clang-format binary in the checkout
5203 try:
5204 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005205 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005206 DieWithError(e)
5207
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005208 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005209 cmd = [clang_format_tool]
5210 if not opts.dry_run and not opts.diff:
5211 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005212 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005213 if opts.diff:
5214 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005215 else:
5216 env = os.environ.copy()
5217 env['PATH'] = str(os.path.dirname(clang_format_tool))
5218 try:
5219 script = clang_format.FindClangFormatScriptInChromiumTree(
5220 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005221 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005222 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005223
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005224 cmd = [sys.executable, script, '-p0']
5225 if not opts.dry_run and not opts.diff:
5226 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005227
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005228 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5229 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005230
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005231 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5232 if opts.diff:
5233 sys.stdout.write(stdout)
5234 if opts.dry_run and len(stdout) > 0:
5235 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005236
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005237 # Similar code to above, but using yapf on .py files rather than clang-format
5238 # on C/C++ files
5239 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005240 yapf_tool = gclient_utils.FindExecutable('yapf')
5241 if yapf_tool is None:
5242 DieWithError('yapf not found in PATH')
5243
5244 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005245 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005246 cmd = [yapf_tool]
5247 if not opts.dry_run and not opts.diff:
5248 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005249 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005250 if opts.diff:
5251 sys.stdout.write(stdout)
5252 else:
5253 # TODO(sbc): yapf --lines mode still has some issues.
5254 # https://github.com/google/yapf/issues/154
5255 DieWithError('--python currently only works with --full')
5256
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005257 # Dart's formatter does not have the nice property of only operating on
5258 # modified chunks, so hard code full.
5259 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005260 try:
5261 command = [dart_format.FindDartFmtToolInChromiumTree()]
5262 if not opts.dry_run and not opts.diff:
5263 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005264 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005265
ppi@chromium.org6593d932016-03-03 15:41:15 +00005266 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005267 if opts.dry_run and stdout:
5268 return_value = 2
5269 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005270 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5271 'found in this checkout. Files in other languages are still '
5272 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005273
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005274 # Format GN build files. Always run on full build files for canonical form.
5275 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005276 cmd = ['gn', 'format' ]
5277 if opts.dry_run or opts.diff:
5278 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005279 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005280 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5281 shell=sys.platform == 'win32',
5282 cwd=top_dir)
5283 if opts.dry_run and gn_ret == 2:
5284 return_value = 2 # Not formatted.
5285 elif opts.diff and gn_ret == 2:
5286 # TODO this should compute and print the actual diff.
5287 print("This change has GN build file diff for " + gn_diff_file)
5288 elif gn_ret != 0:
5289 # For non-dry run cases (and non-2 return values for dry-run), a
5290 # nonzero error code indicates a failure, probably because the file
5291 # doesn't parse.
5292 DieWithError("gn format failed on " + gn_diff_file +
5293 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005294
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005295 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005296
5297
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005298@subcommand.usage('<codereview url or issue id>')
5299def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005300 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005301 _, args = parser.parse_args(args)
5302
5303 if len(args) != 1:
5304 parser.print_help()
5305 return 1
5306
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005307 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005308 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005309 parser.print_help()
5310 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005311 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005312
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005313 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005314 output = RunGit(['config', '--local', '--get-regexp',
5315 r'branch\..*\.%s' % issueprefix],
5316 error_ok=True)
5317 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005318 if issue == target_issue:
5319 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005320
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005321 branches = []
5322 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005323 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005324 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005325 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005326 return 1
5327 if len(branches) == 1:
5328 RunGit(['checkout', branches[0]])
5329 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005330 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005331 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005332 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005333 which = raw_input('Choose by index: ')
5334 try:
5335 RunGit(['checkout', branches[int(which)]])
5336 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005337 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005338 return 1
5339
5340 return 0
5341
5342
maruel@chromium.org29404b52014-09-08 22:58:00 +00005343def CMDlol(parser, args):
5344 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005345 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005346 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5347 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5348 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005349 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005350 return 0
5351
5352
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005353class OptionParser(optparse.OptionParser):
5354 """Creates the option parse and add --verbose support."""
5355 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005356 optparse.OptionParser.__init__(
5357 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005358 self.add_option(
5359 '-v', '--verbose', action='count', default=0,
5360 help='Use 2 times for more debugging info')
5361
5362 def parse_args(self, args=None, values=None):
5363 options, args = optparse.OptionParser.parse_args(self, args, values)
5364 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5365 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5366 return options, args
5367
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005369def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005370 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005371 print('\nYour python version %s is unsupported, please upgrade.\n' %
5372 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005373 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005374
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005375 # Reload settings.
5376 global settings
5377 settings = Settings()
5378
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005379 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005380 dispatcher = subcommand.CommandDispatcher(__name__)
5381 try:
5382 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005383 except auth.AuthenticationError as e:
5384 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005385 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005386 if e.code != 500:
5387 raise
5388 DieWithError(
5389 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5390 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005391 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005392
5393
5394if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005395 # These affect sys.stdout so do it outside of main() to simplify mocks in
5396 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005397 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005398 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005399 try:
5400 sys.exit(main(sys.argv[1:]))
5401 except KeyboardInterrupt:
5402 sys.stderr.write('interrupted\n')
5403 sys.exit(1)