blob: 3e790219a0c56bf747ab38381ba3ad354f5953a6 [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:
scottmgb84b5e32016-11-10 09:25:33 -0800728 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000729 else:
730 similarity_options = ['-M%s' % similarity]
731
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000732 try:
733 stdout = sys.stdout.fileno()
734 except AttributeError:
735 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000737 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000738 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000739 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000740
741
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000742class BuildbucketResponseException(Exception):
743 pass
744
745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746class Settings(object):
747 def __init__(self):
748 self.default_server = None
749 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000750 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 self.is_git_svn = None
752 self.svn_branch = None
753 self.tree_status_url = None
754 self.viewvc_url = None
755 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000756 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000757 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000758 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000759 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000760 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000761 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000762 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700763 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764
765 def LazyUpdateIfNeeded(self):
766 """Updates the settings from a codereview.settings file, if available."""
767 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000768 # The only value that actually changes the behavior is
769 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000770 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000771 error_ok=True
772 ).strip().lower()
773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000775 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 LoadCodereviewSettingsFromFile(cr_settings_file)
777 self.updated = True
778
779 def GetDefaultServerUrl(self, error_ok=False):
780 if not self.default_server:
781 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000782 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000783 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 if error_ok:
785 return self.default_server
786 if not self.default_server:
787 error_message = ('Could not find settings file. You must configure '
788 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000789 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 return self.default_server
792
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000793 @staticmethod
794 def GetRelativeRoot():
795 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000798 if self.root is None:
799 self.root = os.path.abspath(self.GetRelativeRoot())
800 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000802 def GetGitMirror(self, remote='origin'):
803 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000804 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000805 if not os.path.isdir(local_url):
806 return None
807 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
808 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
809 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
810 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
811 if mirror.exists():
812 return mirror
813 return None
814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 def GetIsGitSvn(self):
816 """Return true if this repo looks like it's using git-svn."""
817 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000818 if self.GetPendingRefPrefix():
819 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
820 self.is_git_svn = False
821 else:
822 # If you have any "svn-remote.*" config keys, we think you're using svn.
823 self.is_git_svn = RunGitWithCode(
824 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 return self.is_git_svn
826
827 def GetSVNBranch(self):
828 if self.svn_branch is None:
829 if not self.GetIsGitSvn():
830 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
831
832 # Try to figure out which remote branch we're based on.
833 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000834 # 1) iterate through our branch history and find the svn URL.
835 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837 # regexp matching the git-svn line that contains the URL.
838 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
839
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000840 # We don't want to go through all of history, so read a line from the
841 # pipe at a time.
842 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000843 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000844 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
845 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000846 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000847 for line in proc.stdout:
848 match = git_svn_re.match(line)
849 if match:
850 url = match.group(1)
851 proc.stdout.close() # Cut pipe.
852 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000854 if url:
855 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
856 remotes = RunGit(['config', '--get-regexp',
857 r'^svn-remote\..*\.url']).splitlines()
858 for remote in remotes:
859 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000861 remote = match.group(1)
862 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000863 rewrite_root = RunGit(
864 ['config', 'svn-remote.%s.rewriteRoot' % remote],
865 error_ok=True).strip()
866 if rewrite_root:
867 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000868 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000869 ['config', 'svn-remote.%s.fetch' % remote],
870 error_ok=True).strip()
871 if fetch_spec:
872 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
873 if self.svn_branch:
874 break
875 branch_spec = RunGit(
876 ['config', 'svn-remote.%s.branches' % remote],
877 error_ok=True).strip()
878 if branch_spec:
879 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
880 if self.svn_branch:
881 break
882 tag_spec = RunGit(
883 ['config', 'svn-remote.%s.tags' % remote],
884 error_ok=True).strip()
885 if tag_spec:
886 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
887 if self.svn_branch:
888 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889
890 if not self.svn_branch:
891 DieWithError('Can\'t guess svn branch -- try specifying it on the '
892 'command line')
893
894 return self.svn_branch
895
896 def GetTreeStatusUrl(self, error_ok=False):
897 if not self.tree_status_url:
898 error_message = ('You must configure your tree status URL by running '
899 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000900 self.tree_status_url = self._GetRietveldConfig(
901 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902 return self.tree_status_url
903
904 def GetViewVCUrl(self):
905 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000906 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 return self.viewvc_url
908
rmistry@google.com90752582014-01-14 21:04:50 +0000909 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000910 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000911
rmistry@google.com78948ed2015-07-08 23:09:57 +0000912 def GetIsSkipDependencyUpload(self, branch_name):
913 """Returns true if specified branch should skip dep uploads."""
914 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
915 error_ok=True)
916
rmistry@google.com5626a922015-02-26 14:03:30 +0000917 def GetRunPostUploadHook(self):
918 run_post_upload_hook = self._GetRietveldConfig(
919 'run-post-upload-hook', error_ok=True)
920 return run_post_upload_hook == "True"
921
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000922 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000923 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000924
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000925 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000926 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000927
ukai@chromium.orge8077812012-02-03 03:41:46 +0000928 def GetIsGerrit(self):
929 """Return true if this repo is assosiated with gerrit code review system."""
930 if self.is_gerrit is None:
931 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
932 return self.is_gerrit
933
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000934 def GetSquashGerritUploads(self):
935 """Return true if uploads to Gerrit should be squashed by default."""
936 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700937 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
938 if self.squash_gerrit_uploads is None:
939 # Default is squash now (http://crbug.com/611892#c23).
940 self.squash_gerrit_uploads = not (
941 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
942 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000943 return self.squash_gerrit_uploads
944
tandriia60502f2016-06-20 02:01:53 -0700945 def GetSquashGerritUploadsOverride(self):
946 """Return True or False if codereview.settings should be overridden.
947
948 Returns None if no override has been defined.
949 """
950 # See also http://crbug.com/611892#c23
951 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
952 error_ok=True).strip()
953 if result == 'true':
954 return True
955 if result == 'false':
956 return False
957 return None
958
tandrii@chromium.org28253532016-04-14 13:46:56 +0000959 def GetGerritSkipEnsureAuthenticated(self):
960 """Return True if EnsureAuthenticated should not be done for Gerrit
961 uploads."""
962 if self.gerrit_skip_ensure_authenticated is None:
963 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000964 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000965 error_ok=True).strip() == 'true')
966 return self.gerrit_skip_ensure_authenticated
967
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000968 def GetGitEditor(self):
969 """Return the editor specified in the git config, or None if none is."""
970 if self.git_editor is None:
971 self.git_editor = self._GetConfig('core.editor', error_ok=True)
972 return self.git_editor or None
973
thestig@chromium.org44202a22014-03-11 19:22:18 +0000974 def GetLintRegex(self):
975 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
976 DEFAULT_LINT_REGEX)
977
978 def GetLintIgnoreRegex(self):
979 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
980 DEFAULT_LINT_IGNORE_REGEX)
981
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000982 def GetProject(self):
983 if not self.project:
984 self.project = self._GetRietveldConfig('project', error_ok=True)
985 return self.project
986
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000987 def GetForceHttpsCommitUrl(self):
988 if not self.force_https_commit_url:
989 self.force_https_commit_url = self._GetRietveldConfig(
990 'force-https-commit-url', error_ok=True)
991 return self.force_https_commit_url
992
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000993 def GetPendingRefPrefix(self):
994 if not self.pending_ref_prefix:
995 self.pending_ref_prefix = self._GetRietveldConfig(
996 'pending-ref-prefix', error_ok=True)
997 return self.pending_ref_prefix
998
tandriif46c20f2016-09-14 06:17:05 -0700999 def GetHasGitNumberFooter(self):
1000 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1001 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1002 if not self.git_number_footer:
1003 self.git_number_footer = self._GetRietveldConfig(
1004 'git-number-footer', error_ok=True)
1005 return self.git_number_footer
1006
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001007 def _GetRietveldConfig(self, param, **kwargs):
1008 return self._GetConfig('rietveld.' + param, **kwargs)
1009
rmistry@google.com78948ed2015-07-08 23:09:57 +00001010 def _GetBranchConfig(self, branch_name, param, **kwargs):
1011 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 def _GetConfig(self, param, **kwargs):
1014 self.LazyUpdateIfNeeded()
1015 return RunGit(['config', param], **kwargs).strip()
1016
1017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018def ShortBranchName(branch):
1019 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020 return branch.replace('refs/heads/', '', 1)
1021
1022
1023def GetCurrentBranchRef():
1024 """Returns branch ref (e.g., refs/heads/master) or None."""
1025 return RunGit(['symbolic-ref', 'HEAD'],
1026 stderr=subprocess2.VOID, error_ok=True).strip() or None
1027
1028
1029def GetCurrentBranch():
1030 """Returns current branch or None.
1031
1032 For refs/heads/* branches, returns just last part. For others, full ref.
1033 """
1034 branchref = GetCurrentBranchRef()
1035 if branchref:
1036 return ShortBranchName(branchref)
1037 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038
1039
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001040class _CQState(object):
1041 """Enum for states of CL with respect to Commit Queue."""
1042 NONE = 'none'
1043 DRY_RUN = 'dry_run'
1044 COMMIT = 'commit'
1045
1046 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1047
1048
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001049class _ParsedIssueNumberArgument(object):
1050 def __init__(self, issue=None, patchset=None, hostname=None):
1051 self.issue = issue
1052 self.patchset = patchset
1053 self.hostname = hostname
1054
1055 @property
1056 def valid(self):
1057 return self.issue is not None
1058
1059
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060def ParseIssueNumberArgument(arg):
1061 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1062 fail_result = _ParsedIssueNumberArgument()
1063
1064 if arg.isdigit():
1065 return _ParsedIssueNumberArgument(issue=int(arg))
1066 if not arg.startswith('http'):
1067 return fail_result
1068 url = gclient_utils.UpgradeToHttps(arg)
1069 try:
1070 parsed_url = urlparse.urlparse(url)
1071 except ValueError:
1072 return fail_result
1073 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1074 tmp = cls.ParseIssueURL(parsed_url)
1075 if tmp is not None:
1076 return tmp
1077 return fail_result
1078
1079
tandriic2405f52016-10-10 08:13:15 -07001080class GerritIssueNotExists(Exception):
1081 def __init__(self, issue, url):
1082 self.issue = issue
1083 self.url = url
1084 super(GerritIssueNotExists, self).__init__()
1085
1086 def __str__(self):
1087 return 'issue %s at %s does not exist or you have no access to it' % (
1088 self.issue, self.url)
1089
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001092 """Changelist works with one changelist in local branch.
1093
1094 Supports two codereview backends: Rietveld or Gerrit, selected at object
1095 creation.
1096
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001097 Notes:
1098 * Not safe for concurrent multi-{thread,process} use.
1099 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001100 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001101 """
1102
1103 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1104 """Create a new ChangeList instance.
1105
1106 If issue is given, the codereview must be given too.
1107
1108 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1109 Otherwise, it's decided based on current configuration of the local branch,
1110 with default being 'rietveld' for backwards compatibility.
1111 See _load_codereview_impl for more details.
1112
1113 **kwargs will be passed directly to codereview implementation.
1114 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001116 global settings
1117 if not settings:
1118 # Happens when git_cl.py is used as a utility library.
1119 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001120
1121 if issue:
1122 assert codereview, 'codereview must be known, if issue is known'
1123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 self.branchref = branchref
1125 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001126 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 self.branch = ShortBranchName(self.branchref)
1128 else:
1129 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001131 self.lookedup_issue = False
1132 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 self.has_description = False
1134 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001135 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001137 self.cc = None
1138 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001139 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001140
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001142 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001144 assert self._codereview_impl
1145 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001146
1147 def _load_codereview_impl(self, codereview=None, **kwargs):
1148 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001149 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1150 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1151 self._codereview = codereview
1152 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001153 return
1154
1155 # Automatic selection based on issue number set for a current branch.
1156 # Rietveld takes precedence over Gerrit.
1157 assert not self.issue
1158 # Whether we find issue or not, we are doing the lookup.
1159 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001160 if self.GetBranch():
1161 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1162 issue = _git_get_branch_config_value(
1163 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1164 if issue:
1165 self._codereview = codereview
1166 self._codereview_impl = cls(self, **kwargs)
1167 self.issue = int(issue)
1168 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169
1170 # No issue is set for this branch, so decide based on repo-wide settings.
1171 return self._load_codereview_impl(
1172 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1173 **kwargs)
1174
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001175 def IsGerrit(self):
1176 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001177
1178 def GetCCList(self):
1179 """Return the users cc'd on this CL.
1180
agable92bec4f2016-08-24 09:27:27 -07001181 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001182 """
1183 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001184 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001185 more_cc = ','.join(self.watchers)
1186 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1187 return self.cc
1188
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001189 def GetCCListWithoutDefault(self):
1190 """Return the users cc'd on this CL excluding default ones."""
1191 if self.cc is None:
1192 self.cc = ','.join(self.watchers)
1193 return self.cc
1194
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001195 def SetWatchers(self, watchers):
1196 """Set the list of email addresses that should be cc'd based on the changed
1197 files in this CL.
1198 """
1199 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200
1201 def GetBranch(self):
1202 """Returns the short branch name, e.g. 'master'."""
1203 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001204 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001205 if not branchref:
1206 return None
1207 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 self.branch = ShortBranchName(self.branchref)
1209 return self.branch
1210
1211 def GetBranchRef(self):
1212 """Returns the full branch name, e.g. 'refs/heads/master'."""
1213 self.GetBranch() # Poke the lazy loader.
1214 return self.branchref
1215
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001216 def ClearBranch(self):
1217 """Clears cached branch data of this object."""
1218 self.branch = self.branchref = None
1219
tandrii5d48c322016-08-18 16:19:37 -07001220 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1221 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1222 kwargs['branch'] = self.GetBranch()
1223 return _git_get_branch_config_value(key, default, **kwargs)
1224
1225 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1226 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1227 assert self.GetBranch(), (
1228 'this CL must have an associated branch to %sset %s%s' %
1229 ('un' if value is None else '',
1230 key,
1231 '' if value is None else ' to %r' % value))
1232 kwargs['branch'] = self.GetBranch()
1233 return _git_set_branch_config_value(key, value, **kwargs)
1234
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001235 @staticmethod
1236 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001237 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 e.g. 'origin', 'refs/heads/master'
1239 """
1240 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001241 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001244 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001246 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1247 error_ok=True).strip()
1248 if upstream_branch:
1249 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001251 # Fall back on trying a git-svn upstream branch.
1252 if settings.GetIsGitSvn():
1253 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001255 # Else, try to guess the origin remote.
1256 remote_branches = RunGit(['branch', '-r']).split()
1257 if 'origin/master' in remote_branches:
1258 # Fall back on origin/master if it exits.
1259 remote = 'origin'
1260 upstream_branch = 'refs/heads/master'
1261 elif 'origin/trunk' in remote_branches:
1262 # Fall back on origin/trunk if it exists. Generally a shared
1263 # git-svn clone
1264 remote = 'origin'
1265 upstream_branch = 'refs/heads/trunk'
1266 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001267 DieWithError(
1268 'Unable to determine default branch to diff against.\n'
1269 'Either pass complete "git diff"-style arguments, like\n'
1270 ' git cl upload origin/master\n'
1271 'or verify this branch is set up to track another \n'
1272 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273
1274 return remote, upstream_branch
1275
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001276 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001277 upstream_branch = self.GetUpstreamBranch()
1278 if not BranchExists(upstream_branch):
1279 DieWithError('The upstream for the current branch (%s) does not exist '
1280 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001281 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001282 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 def GetUpstreamBranch(self):
1285 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001288 upstream_branch = upstream_branch.replace('refs/heads/',
1289 'refs/remotes/%s/' % remote)
1290 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1291 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292 self.upstream_branch = upstream_branch
1293 return self.upstream_branch
1294
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001295 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001296 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001297 remote, branch = None, self.GetBranch()
1298 seen_branches = set()
1299 while branch not in seen_branches:
1300 seen_branches.add(branch)
1301 remote, branch = self.FetchUpstreamTuple(branch)
1302 branch = ShortBranchName(branch)
1303 if remote != '.' or branch.startswith('refs/remotes'):
1304 break
1305 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001306 remotes = RunGit(['remote'], error_ok=True).split()
1307 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001310 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001311 logging.warning('Could not determine which remote this change is '
1312 'associated with, so defaulting to "%s". This may '
1313 'not be what you want. You may prevent this message '
1314 'by running "git svn info" as documented here: %s',
1315 self._remote,
1316 GIT_INSTRUCTIONS_URL)
1317 else:
1318 logging.warn('Could not determine which remote this change is '
1319 'associated with. You may prevent this message by '
1320 'running "git svn info" as documented here: %s',
1321 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 branch = 'HEAD'
1323 if branch.startswith('refs/remotes'):
1324 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001325 elif branch.startswith('refs/branch-heads/'):
1326 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 else:
1328 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329 return self._remote
1330
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 def GitSanityChecks(self, upstream_git_obj):
1332 """Checks git repo status and ensures diff is from local commits."""
1333
sbc@chromium.org79706062015-01-14 21:18:12 +00001334 if upstream_git_obj is None:
1335 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001336 print('ERROR: unable to determine current branch (detached HEAD?)',
1337 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001339 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001340 return False
1341
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001342 # Verify the commit we're diffing against is in our current branch.
1343 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1344 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1345 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001346 print('ERROR: %s is not in the current branch. You may need to rebase '
1347 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001348 return False
1349
1350 # List the commits inside the diff, and verify they are all local.
1351 commits_in_diff = RunGit(
1352 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1353 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1354 remote_branch = remote_branch.strip()
1355 if code != 0:
1356 _, remote_branch = self.GetRemoteBranch()
1357
1358 commits_in_remote = RunGit(
1359 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1360
1361 common_commits = set(commits_in_diff) & set(commits_in_remote)
1362 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001363 print('ERROR: Your diff contains %d commits already in %s.\n'
1364 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1365 'the diff. If you are using a custom git flow, you can override'
1366 ' the reference used for this check with "git config '
1367 'gitcl.remotebranch <git-ref>".' % (
1368 len(common_commits), remote_branch, upstream_git_obj),
1369 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001370 return False
1371 return True
1372
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001373 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001374 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001375
1376 Returns None if it is not set.
1377 """
tandrii5d48c322016-08-18 16:19:37 -07001378 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001379
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001380 def GetGitSvnRemoteUrl(self):
1381 """Return the configured git-svn remote URL parsed from git svn info.
1382
1383 Returns None if it is not set.
1384 """
1385 # URL is dependent on the current directory.
1386 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1387 if data:
1388 keys = dict(line.split(': ', 1) for line in data.splitlines()
1389 if ': ' in line)
1390 return keys.get('URL', None)
1391 return None
1392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 def GetRemoteUrl(self):
1394 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1395
1396 Returns None if there is no remote.
1397 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001398 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001399 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1400
1401 # If URL is pointing to a local directory, it is probably a git cache.
1402 if os.path.isdir(url):
1403 url = RunGit(['config', 'remote.%s.url' % remote],
1404 error_ok=True,
1405 cwd=url).strip()
1406 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001408 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001409 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001410 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001411 self.issue = self._GitGetBranchConfigValue(
1412 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001413 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 return self.issue
1415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 def GetIssueURL(self):
1417 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001418 issue = self.GetIssue()
1419 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001420 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
1423 def GetDescription(self, pretty=False):
1424 if not self.has_description:
1425 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001426 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 self.has_description = True
1428 if pretty:
1429 wrapper = textwrap.TextWrapper()
1430 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1431 return wrapper.fill(self.description)
1432 return self.description
1433
1434 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001435 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001436 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001437 self.patchset = self._GitGetBranchConfigValue(
1438 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 return self.patchset
1441
1442 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001443 """Set this branch's patchset. If patchset=0, clears the patchset."""
1444 assert self.GetBranch()
1445 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001446 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001447 else:
1448 self.patchset = int(patchset)
1449 self._GitSetBranchConfigValue(
1450 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001452 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001453 """Set this branch's issue. If issue isn't given, clears the issue."""
1454 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001456 issue = int(issue)
1457 self._GitSetBranchConfigValue(
1458 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001459 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001460 codereview_server = self._codereview_impl.GetCodereviewServer()
1461 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001462 self._GitSetBranchConfigValue(
1463 self._codereview_impl.CodereviewServerConfigKey(),
1464 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 else:
tandrii5d48c322016-08-18 16:19:37 -07001466 # Reset all of these just to be clean.
1467 reset_suffixes = [
1468 'last-upload-hash',
1469 self._codereview_impl.IssueConfigKey(),
1470 self._codereview_impl.PatchsetConfigKey(),
1471 self._codereview_impl.CodereviewServerConfigKey(),
1472 ] + self._PostUnsetIssueProperties()
1473 for prop in reset_suffixes:
1474 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001475 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001476 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477
dnjba1b0f32016-09-02 12:37:42 -07001478 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001479 if not self.GitSanityChecks(upstream_branch):
1480 DieWithError('\nGit sanity check failure')
1481
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001482 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001483 if not root:
1484 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001485 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001486
1487 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001488 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001489 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001490 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001491 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001492 except subprocess2.CalledProcessError:
1493 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001494 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001495 'This branch probably doesn\'t exist anymore. To reset the\n'
1496 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001497 ' git branch --set-upstream-to origin/master %s\n'
1498 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001500
maruel@chromium.org52424302012-08-29 15:14:30 +00001501 issue = self.GetIssue()
1502 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001503 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504 description = self.GetDescription()
1505 else:
1506 # If the change was never uploaded, use the log messages of all commits
1507 # up to the branch point, as git cl upload will prefill the description
1508 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001509 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1510 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001511
1512 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001513 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001514 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001515 name,
1516 description,
1517 absroot,
1518 files,
1519 issue,
1520 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001521 author,
1522 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001523
dsansomee2d6fd92016-09-08 00:10:47 -07001524 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001526 return self._codereview_impl.UpdateDescriptionRemote(
1527 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001528
1529 def RunHook(self, committing, may_prompt, verbose, change):
1530 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1531 try:
1532 return presubmit_support.DoPresubmitChecks(change, committing,
1533 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1534 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001535 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1536 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001537 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001538 DieWithError(
1539 ('%s\nMaybe your depot_tools is out of date?\n'
1540 'If all fails, contact maruel@') % e)
1541
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001542 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1543 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001544 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1545 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001546 else:
1547 # Assume url.
1548 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1549 urlparse.urlparse(issue_arg))
1550 if not parsed_issue_arg or not parsed_issue_arg.valid:
1551 DieWithError('Failed to parse issue argument "%s". '
1552 'Must be an issue number or a valid URL.' % issue_arg)
1553 return self._codereview_impl.CMDPatchWithParsedIssue(
1554 parsed_issue_arg, reject, nocommit, directory)
1555
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001556 def CMDUpload(self, options, git_diff_args, orig_args):
1557 """Uploads a change to codereview."""
1558 if git_diff_args:
1559 # TODO(ukai): is it ok for gerrit case?
1560 base_branch = git_diff_args[0]
1561 else:
1562 if self.GetBranch() is None:
1563 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1564
1565 # Default to diffing against common ancestor of upstream branch
1566 base_branch = self.GetCommonAncestorWithUpstream()
1567 git_diff_args = [base_branch, 'HEAD']
1568
1569 # Make sure authenticated to codereview before running potentially expensive
1570 # hooks. It is a fast, best efforts check. Codereview still can reject the
1571 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001572 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573
1574 # Apply watchlists on upload.
1575 change = self.GetChange(base_branch, None)
1576 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1577 files = [f.LocalPath() for f in change.AffectedFiles()]
1578 if not options.bypass_watchlists:
1579 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1580
1581 if not options.bypass_hooks:
1582 if options.reviewers or options.tbr_owners:
1583 # Set the reviewer list now so that presubmit checks can access it.
1584 change_description = ChangeDescription(change.FullDescriptionText())
1585 change_description.update_reviewers(options.reviewers,
1586 options.tbr_owners,
1587 change)
1588 change.SetDescriptionText(change_description.description)
1589 hook_results = self.RunHook(committing=False,
1590 may_prompt=not options.force,
1591 verbose=options.verbose,
1592 change=change)
1593 if not hook_results.should_continue():
1594 return 1
1595 if not options.reviewers and hook_results.reviewers:
1596 options.reviewers = hook_results.reviewers.split(',')
1597
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001598 # TODO(tandrii): Checking local patchset against remote patchset is only
1599 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1600 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601 latest_patchset = self.GetMostRecentPatchset()
1602 local_patchset = self.GetPatchset()
1603 if (latest_patchset and local_patchset and
1604 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001605 print('The last upload made from this repository was patchset #%d but '
1606 'the most recent patchset on the server is #%d.'
1607 % (local_patchset, latest_patchset))
1608 print('Uploading will still work, but if you\'ve uploaded to this '
1609 'issue from another machine or branch the patch you\'re '
1610 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001611 ask_for_data('About to upload; enter to confirm.')
1612
1613 print_stats(options.similarity, options.find_copies, git_diff_args)
1614 ret = self.CMDUploadChange(options, git_diff_args, change)
1615 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001616 if options.use_commit_queue:
1617 self.SetCQState(_CQState.COMMIT)
1618 elif options.cq_dry_run:
1619 self.SetCQState(_CQState.DRY_RUN)
1620
tandrii5d48c322016-08-18 16:19:37 -07001621 _git_set_branch_config_value('last-upload-hash',
1622 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 # Run post upload hooks, if specified.
1624 if settings.GetRunPostUploadHook():
1625 presubmit_support.DoPostUploadExecuter(
1626 change,
1627 self,
1628 settings.GetRoot(),
1629 options.verbose,
1630 sys.stdout)
1631
1632 # Upload all dependencies if specified.
1633 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001634 print()
1635 print('--dependencies has been specified.')
1636 print('All dependent local branches will be re-uploaded.')
1637 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001638 # Remove the dependencies flag from args so that we do not end up in a
1639 # loop.
1640 orig_args.remove('--dependencies')
1641 ret = upload_branch_deps(self, orig_args)
1642 return ret
1643
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001644 def SetCQState(self, new_state):
1645 """Update the CQ state for latest patchset.
1646
1647 Issue must have been already uploaded and known.
1648 """
1649 assert new_state in _CQState.ALL_STATES
1650 assert self.GetIssue()
1651 return self._codereview_impl.SetCQState(new_state)
1652
qyearsley1fdfcb62016-10-24 13:22:03 -07001653 def TriggerDryRun(self):
1654 """Triggers a dry run and prints a warning on failure."""
1655 # TODO(qyearsley): Either re-use this method in CMDset_commit
1656 # and CMDupload, or change CMDtry to trigger dry runs with
1657 # just SetCQState, and catch keyboard interrupt and other
1658 # errors in that method.
1659 try:
1660 self.SetCQState(_CQState.DRY_RUN)
1661 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1662 return 0
1663 except KeyboardInterrupt:
1664 raise
1665 except:
1666 print('WARNING: failed to trigger CQ Dry Run.\n'
1667 'Either:\n'
1668 ' * your project has no CQ\n'
1669 ' * you don\'t have permission to trigger Dry Run\n'
1670 ' * bug in this code (see stack trace below).\n'
1671 'Consider specifying which bots to trigger manually '
1672 'or asking your project owners for permissions '
1673 'or contacting Chrome Infrastructure team at '
1674 'https://www.chromium.org/infra\n\n')
1675 # Still raise exception so that stack trace is printed.
1676 raise
1677
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001678 # Forward methods to codereview specific implementation.
1679
1680 def CloseIssue(self):
1681 return self._codereview_impl.CloseIssue()
1682
1683 def GetStatus(self):
1684 return self._codereview_impl.GetStatus()
1685
1686 def GetCodereviewServer(self):
1687 return self._codereview_impl.GetCodereviewServer()
1688
tandriide281ae2016-10-12 06:02:30 -07001689 def GetIssueOwner(self):
1690 """Get owner from codereview, which may differ from this checkout."""
1691 return self._codereview_impl.GetIssueOwner()
1692
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693 def GetApprovingReviewers(self):
1694 return self._codereview_impl.GetApprovingReviewers()
1695
1696 def GetMostRecentPatchset(self):
1697 return self._codereview_impl.GetMostRecentPatchset()
1698
tandriide281ae2016-10-12 06:02:30 -07001699 def CannotTriggerTryJobReason(self):
1700 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1701 return self._codereview_impl.CannotTriggerTryJobReason()
1702
tandrii8c5a3532016-11-04 07:52:02 -07001703 def GetTryjobProperties(self, patchset=None):
1704 """Returns dictionary of properties to launch tryjob."""
1705 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1706
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001707 def __getattr__(self, attr):
1708 # This is because lots of untested code accesses Rietveld-specific stuff
1709 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001710 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001711 # Note that child method defines __getattr__ as well, and forwards it here,
1712 # because _RietveldChangelistImpl is not cleaned up yet, and given
1713 # deprecation of Rietveld, it should probably be just removed.
1714 # Until that time, avoid infinite recursion by bypassing __getattr__
1715 # of implementation class.
1716 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717
1718
1719class _ChangelistCodereviewBase(object):
1720 """Abstract base class encapsulating codereview specifics of a changelist."""
1721 def __init__(self, changelist):
1722 self._changelist = changelist # instance of Changelist
1723
1724 def __getattr__(self, attr):
1725 # Forward methods to changelist.
1726 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1727 # _RietveldChangelistImpl to avoid this hack?
1728 return getattr(self._changelist, attr)
1729
1730 def GetStatus(self):
1731 """Apply a rough heuristic to give a simple summary of an issue's review
1732 or CQ status, assuming adherence to a common workflow.
1733
1734 Returns None if no issue for this branch, or specific string keywords.
1735 """
1736 raise NotImplementedError()
1737
1738 def GetCodereviewServer(self):
1739 """Returns server URL without end slash, like "https://codereview.com"."""
1740 raise NotImplementedError()
1741
1742 def FetchDescription(self):
1743 """Fetches and returns description from the codereview server."""
1744 raise NotImplementedError()
1745
tandrii5d48c322016-08-18 16:19:37 -07001746 @classmethod
1747 def IssueConfigKey(cls):
1748 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001749 raise NotImplementedError()
1750
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001751 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001752 def PatchsetConfigKey(cls):
1753 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 raise NotImplementedError()
1755
tandrii5d48c322016-08-18 16:19:37 -07001756 @classmethod
1757 def CodereviewServerConfigKey(cls):
1758 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 raise NotImplementedError()
1760
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001761 def _PostUnsetIssueProperties(self):
1762 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001763 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001764
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765 def GetRieveldObjForPresubmit(self):
1766 # This is an unfortunate Rietveld-embeddedness in presubmit.
1767 # For non-Rietveld codereviews, this probably should return a dummy object.
1768 raise NotImplementedError()
1769
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001770 def GetGerritObjForPresubmit(self):
1771 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1772 return None
1773
dsansomee2d6fd92016-09-08 00:10:47 -07001774 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001775 """Update the description on codereview site."""
1776 raise NotImplementedError()
1777
1778 def CloseIssue(self):
1779 """Closes the issue."""
1780 raise NotImplementedError()
1781
1782 def GetApprovingReviewers(self):
1783 """Returns a list of reviewers approving the change.
1784
1785 Note: not necessarily committers.
1786 """
1787 raise NotImplementedError()
1788
1789 def GetMostRecentPatchset(self):
1790 """Returns the most recent patchset number from the codereview site."""
1791 raise NotImplementedError()
1792
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001793 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1794 directory):
1795 """Fetches and applies the issue.
1796
1797 Arguments:
1798 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1799 reject: if True, reject the failed patch instead of switching to 3-way
1800 merge. Rietveld only.
1801 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1802 only.
1803 directory: switch to directory before applying the patch. Rietveld only.
1804 """
1805 raise NotImplementedError()
1806
1807 @staticmethod
1808 def ParseIssueURL(parsed_url):
1809 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1810 failed."""
1811 raise NotImplementedError()
1812
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001813 def EnsureAuthenticated(self, force):
1814 """Best effort check that user is authenticated with codereview server.
1815
1816 Arguments:
1817 force: whether to skip confirmation questions.
1818 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001819 raise NotImplementedError()
1820
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001821 def CMDUploadChange(self, options, args, change):
1822 """Uploads a change to codereview."""
1823 raise NotImplementedError()
1824
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001825 def SetCQState(self, new_state):
1826 """Update the CQ state for latest patchset.
1827
1828 Issue must have been already uploaded and known.
1829 """
1830 raise NotImplementedError()
1831
tandriie113dfd2016-10-11 10:20:12 -07001832 def CannotTriggerTryJobReason(self):
1833 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1834 raise NotImplementedError()
1835
tandriide281ae2016-10-12 06:02:30 -07001836 def GetIssueOwner(self):
1837 raise NotImplementedError()
1838
tandrii8c5a3532016-11-04 07:52:02 -07001839 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001840 raise NotImplementedError()
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842
1843class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1844 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1845 super(_RietveldChangelistImpl, self).__init__(changelist)
1846 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001847 if not rietveld_server:
1848 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849
1850 self._rietveld_server = rietveld_server
1851 self._auth_config = auth_config
1852 self._props = None
1853 self._rpc_server = None
1854
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001855 def GetCodereviewServer(self):
1856 if not self._rietveld_server:
1857 # If we're on a branch then get the server potentially associated
1858 # with that branch.
1859 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001860 self._rietveld_server = gclient_utils.UpgradeToHttps(
1861 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001862 if not self._rietveld_server:
1863 self._rietveld_server = settings.GetDefaultServerUrl()
1864 return self._rietveld_server
1865
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001866 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001867 """Best effort check that user is authenticated with Rietveld server."""
1868 if self._auth_config.use_oauth2:
1869 authenticator = auth.get_authenticator_for_host(
1870 self.GetCodereviewServer(), self._auth_config)
1871 if not authenticator.has_cached_credentials():
1872 raise auth.LoginRequiredError(self.GetCodereviewServer())
1873
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001874 def FetchDescription(self):
1875 issue = self.GetIssue()
1876 assert issue
1877 try:
1878 return self.RpcServer().get_description(issue).strip()
1879 except urllib2.HTTPError as e:
1880 if e.code == 404:
1881 DieWithError(
1882 ('\nWhile fetching the description for issue %d, received a '
1883 '404 (not found)\n'
1884 'error. It is likely that you deleted this '
1885 'issue on the server. If this is the\n'
1886 'case, please run\n\n'
1887 ' git cl issue 0\n\n'
1888 'to clear the association with the deleted issue. Then run '
1889 'this command again.') % issue)
1890 else:
1891 DieWithError(
1892 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1893 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001894 print('Warning: Failed to retrieve CL description due to network '
1895 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001896 return ''
1897
1898 def GetMostRecentPatchset(self):
1899 return self.GetIssueProperties()['patchsets'][-1]
1900
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001901 def GetIssueProperties(self):
1902 if self._props is None:
1903 issue = self.GetIssue()
1904 if not issue:
1905 self._props = {}
1906 else:
1907 self._props = self.RpcServer().get_issue_properties(issue, True)
1908 return self._props
1909
tandriie113dfd2016-10-11 10:20:12 -07001910 def CannotTriggerTryJobReason(self):
1911 props = self.GetIssueProperties()
1912 if not props:
1913 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1914 if props.get('closed'):
1915 return 'CL %s is closed' % self.GetIssue()
1916 if props.get('private'):
1917 return 'CL %s is private' % self.GetIssue()
1918 return None
1919
tandrii8c5a3532016-11-04 07:52:02 -07001920 def GetTryjobProperties(self, patchset=None):
1921 """Returns dictionary of properties to launch tryjob."""
1922 project = (self.GetIssueProperties() or {}).get('project')
1923 return {
1924 'issue': self.GetIssue(),
1925 'patch_project': project,
1926 'patch_storage': 'rietveld',
1927 'patchset': patchset or self.GetPatchset(),
1928 'rietveld': self.GetCodereviewServer(),
1929 }
1930
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931 def GetApprovingReviewers(self):
1932 return get_approving_reviewers(self.GetIssueProperties())
1933
tandriide281ae2016-10-12 06:02:30 -07001934 def GetIssueOwner(self):
1935 return (self.GetIssueProperties() or {}).get('owner_email')
1936
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001937 def AddComment(self, message):
1938 return self.RpcServer().add_comment(self.GetIssue(), message)
1939
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001940 def GetStatus(self):
1941 """Apply a rough heuristic to give a simple summary of an issue's review
1942 or CQ status, assuming adherence to a common workflow.
1943
1944 Returns None if no issue for this branch, or one of the following keywords:
1945 * 'error' - error from review tool (including deleted issues)
1946 * 'unsent' - not sent for review
1947 * 'waiting' - waiting for review
1948 * 'reply' - waiting for owner to reply to review
1949 * 'lgtm' - LGTM from at least one approved reviewer
1950 * 'commit' - in the commit queue
1951 * 'closed' - closed
1952 """
1953 if not self.GetIssue():
1954 return None
1955
1956 try:
1957 props = self.GetIssueProperties()
1958 except urllib2.HTTPError:
1959 return 'error'
1960
1961 if props.get('closed'):
1962 # Issue is closed.
1963 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001964 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001965 # Issue is in the commit queue.
1966 return 'commit'
1967
1968 try:
1969 reviewers = self.GetApprovingReviewers()
1970 except urllib2.HTTPError:
1971 return 'error'
1972
1973 if reviewers:
1974 # Was LGTM'ed.
1975 return 'lgtm'
1976
1977 messages = props.get('messages') or []
1978
tandrii9d2c7a32016-06-22 03:42:45 -07001979 # Skip CQ messages that don't require owner's action.
1980 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1981 if 'Dry run:' in messages[-1]['text']:
1982 messages.pop()
1983 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1984 # This message always follows prior messages from CQ,
1985 # so skip this too.
1986 messages.pop()
1987 else:
1988 # This is probably a CQ messages warranting user attention.
1989 break
1990
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001991 if not messages:
1992 # No message was sent.
1993 return 'unsent'
1994 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001995 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001996 return 'reply'
1997 return 'waiting'
1998
dsansomee2d6fd92016-09-08 00:10:47 -07001999 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002000 return self.RpcServer().update_description(
2001 self.GetIssue(), self.description)
2002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002003 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002004 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002006 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002007 return self.SetFlags({flag: value})
2008
2009 def SetFlags(self, flags):
2010 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002011 """
phajdan.jr68598232016-08-10 03:28:28 -07002012 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002013 try:
tandrii4b233bd2016-07-06 03:50:29 -07002014 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002015 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002016 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002017 if e.code == 404:
2018 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2019 if e.code == 403:
2020 DieWithError(
2021 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002022 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002023 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002025 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 """Returns an upload.RpcServer() to access this review's rietveld instance.
2027 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002028 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002029 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002030 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002031 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002032 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002034 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002035 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002036 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037
tandrii5d48c322016-08-18 16:19:37 -07002038 @classmethod
2039 def PatchsetConfigKey(cls):
2040 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041
tandrii5d48c322016-08-18 16:19:37 -07002042 @classmethod
2043 def CodereviewServerConfigKey(cls):
2044 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002045
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002046 def GetRieveldObjForPresubmit(self):
2047 return self.RpcServer()
2048
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002049 def SetCQState(self, new_state):
2050 props = self.GetIssueProperties()
2051 if props.get('private'):
2052 DieWithError('Cannot set-commit on private issue')
2053
2054 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002055 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002056 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002057 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002058 else:
tandrii4b233bd2016-07-06 03:50:29 -07002059 assert new_state == _CQState.DRY_RUN
2060 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002061
2062
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002063 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2064 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002065 # PatchIssue should never be called with a dirty tree. It is up to the
2066 # caller to check this, but just in case we assert here since the
2067 # consequences of the caller not checking this could be dire.
2068 assert(not git_common.is_dirty_git_tree('apply'))
2069 assert(parsed_issue_arg.valid)
2070 self._changelist.issue = parsed_issue_arg.issue
2071 if parsed_issue_arg.hostname:
2072 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2073
skobes6468b902016-10-24 08:45:10 -07002074 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2075 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2076 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002077 try:
skobes6468b902016-10-24 08:45:10 -07002078 scm_obj.apply_patch(patchset_object)
2079 except Exception as e:
2080 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 return 1
2082
2083 # If we had an issue, commit the current state and register the issue.
2084 if not nocommit:
2085 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2086 'patch from issue %(i)s at patchset '
2087 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2088 % {'i': self.GetIssue(), 'p': patchset})])
2089 self.SetIssue(self.GetIssue())
2090 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002091 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002092 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002093 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002094 return 0
2095
2096 @staticmethod
2097 def ParseIssueURL(parsed_url):
2098 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2099 return None
wychen3c1c1722016-08-04 11:46:36 -07002100 # Rietveld patch: https://domain/<number>/#ps<patchset>
2101 match = re.match(r'/(\d+)/$', parsed_url.path)
2102 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2103 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002104 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002105 issue=int(match.group(1)),
2106 patchset=int(match2.group(1)),
2107 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002108 # Typical url: https://domain/<issue_number>[/[other]]
2109 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2110 if match:
skobes6468b902016-10-24 08:45:10 -07002111 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 issue=int(match.group(1)),
2113 hostname=parsed_url.netloc)
2114 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2115 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2116 if match:
skobes6468b902016-10-24 08:45:10 -07002117 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 issue=int(match.group(1)),
2119 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002120 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002121 return None
2122
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002123 def CMDUploadChange(self, options, args, change):
2124 """Upload the patch to Rietveld."""
2125 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2126 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002127 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2128 if options.emulate_svn_auto_props:
2129 upload_args.append('--emulate_svn_auto_props')
2130
2131 change_desc = None
2132
2133 if options.email is not None:
2134 upload_args.extend(['--email', options.email])
2135
2136 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002137 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002138 upload_args.extend(['--title', options.title])
2139 if options.message:
2140 upload_args.extend(['--message', options.message])
2141 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002142 print('This branch is associated with issue %s. '
2143 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002144 else:
nodirca166002016-06-27 10:59:51 -07002145 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146 upload_args.extend(['--title', options.title])
2147 message = (options.title or options.message or
2148 CreateDescriptionFromLog(args))
2149 change_desc = ChangeDescription(message)
2150 if options.reviewers or options.tbr_owners:
2151 change_desc.update_reviewers(options.reviewers,
2152 options.tbr_owners,
2153 change)
2154 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002155 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002156
2157 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002158 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002159 return 1
2160
2161 upload_args.extend(['--message', change_desc.description])
2162 if change_desc.get_reviewers():
2163 upload_args.append('--reviewers=%s' % ','.join(
2164 change_desc.get_reviewers()))
2165 if options.send_mail:
2166 if not change_desc.get_reviewers():
2167 DieWithError("Must specify reviewers to send email.")
2168 upload_args.append('--send_mail')
2169
2170 # We check this before applying rietveld.private assuming that in
2171 # rietveld.cc only addresses which we can send private CLs to are listed
2172 # if rietveld.private is set, and so we should ignore rietveld.cc only
2173 # when --private is specified explicitly on the command line.
2174 if options.private:
2175 logging.warn('rietveld.cc is ignored since private flag is specified. '
2176 'You need to review and add them manually if necessary.')
2177 cc = self.GetCCListWithoutDefault()
2178 else:
2179 cc = self.GetCCList()
2180 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002181 if change_desc.get_cced():
2182 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 if cc:
2184 upload_args.extend(['--cc', cc])
2185
2186 if options.private or settings.GetDefaultPrivateFlag() == "True":
2187 upload_args.append('--private')
2188
2189 upload_args.extend(['--git_similarity', str(options.similarity)])
2190 if not options.find_copies:
2191 upload_args.extend(['--git_no_find_copies'])
2192
2193 # Include the upstream repo's URL in the change -- this is useful for
2194 # projects that have their source spread across multiple repos.
2195 remote_url = self.GetGitBaseUrlFromConfig()
2196 if not remote_url:
2197 if settings.GetIsGitSvn():
2198 remote_url = self.GetGitSvnRemoteUrl()
2199 else:
2200 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2201 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2202 self.GetUpstreamBranch().split('/')[-1])
2203 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002204 remote, remote_branch = self.GetRemoteBranch()
2205 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2206 settings.GetPendingRefPrefix())
2207 if target_ref:
2208 upload_args.extend(['--target_ref', target_ref])
2209
2210 # Look for dependent patchsets. See crbug.com/480453 for more details.
2211 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2212 upstream_branch = ShortBranchName(upstream_branch)
2213 if remote is '.':
2214 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002215 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002217 print()
2218 print('Skipping dependency patchset upload because git config '
2219 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2220 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002221 else:
2222 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002223 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 auth_config=auth_config)
2225 branch_cl_issue_url = branch_cl.GetIssueURL()
2226 branch_cl_issue = branch_cl.GetIssue()
2227 branch_cl_patchset = branch_cl.GetPatchset()
2228 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2229 upload_args.extend(
2230 ['--depends_on_patchset', '%s:%s' % (
2231 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002232 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 '\n'
2234 'The current branch (%s) is tracking a local branch (%s) with '
2235 'an associated CL.\n'
2236 'Adding %s/#ps%s as a dependency patchset.\n'
2237 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2238 branch_cl_patchset))
2239
2240 project = settings.GetProject()
2241 if project:
2242 upload_args.extend(['--project', project])
2243
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 try:
2245 upload_args = ['upload'] + upload_args + args
2246 logging.info('upload.RealMain(%s)', upload_args)
2247 issue, patchset = upload.RealMain(upload_args)
2248 issue = int(issue)
2249 patchset = int(patchset)
2250 except KeyboardInterrupt:
2251 sys.exit(1)
2252 except:
2253 # If we got an exception after the user typed a description for their
2254 # change, back up the description before re-raising.
2255 if change_desc:
2256 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2257 print('\nGot exception while uploading -- saving description to %s\n' %
2258 backup_path)
2259 backup_file = open(backup_path, 'w')
2260 backup_file.write(change_desc.description)
2261 backup_file.close()
2262 raise
2263
2264 if not self.GetIssue():
2265 self.SetIssue(issue)
2266 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002267 return 0
2268
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002269
2270class _GerritChangelistImpl(_ChangelistCodereviewBase):
2271 def __init__(self, changelist, auth_config=None):
2272 # auth_config is Rietveld thing, kept here to preserve interface only.
2273 super(_GerritChangelistImpl, self).__init__(changelist)
2274 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002275 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002276 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002277 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002278
2279 def _GetGerritHost(self):
2280 # Lazy load of configs.
2281 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002282 if self._gerrit_host and '.' not in self._gerrit_host:
2283 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2284 # This happens for internal stuff http://crbug.com/614312.
2285 parsed = urlparse.urlparse(self.GetRemoteUrl())
2286 if parsed.scheme == 'sso':
2287 print('WARNING: using non https URLs for remote is likely broken\n'
2288 ' Your current remote is: %s' % self.GetRemoteUrl())
2289 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2290 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002291 return self._gerrit_host
2292
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002293 def _GetGitHost(self):
2294 """Returns git host to be used when uploading change to Gerrit."""
2295 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2296
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002297 def GetCodereviewServer(self):
2298 if not self._gerrit_server:
2299 # If we're on a branch then get the server potentially associated
2300 # with that branch.
2301 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002302 self._gerrit_server = self._GitGetBranchConfigValue(
2303 self.CodereviewServerConfigKey())
2304 if self._gerrit_server:
2305 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306 if not self._gerrit_server:
2307 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2308 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002309 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002310 parts[0] = parts[0] + '-review'
2311 self._gerrit_host = '.'.join(parts)
2312 self._gerrit_server = 'https://%s' % self._gerrit_host
2313 return self._gerrit_server
2314
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002315 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002316 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002317 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002318
tandrii5d48c322016-08-18 16:19:37 -07002319 @classmethod
2320 def PatchsetConfigKey(cls):
2321 return 'gerritpatchset'
2322
2323 @classmethod
2324 def CodereviewServerConfigKey(cls):
2325 return 'gerritserver'
2326
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002327 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002328 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002329 if settings.GetGerritSkipEnsureAuthenticated():
2330 # For projects with unusual authentication schemes.
2331 # See http://crbug.com/603378.
2332 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002333 # Lazy-loader to identify Gerrit and Git hosts.
2334 if gerrit_util.GceAuthenticator.is_gce():
2335 return
2336 self.GetCodereviewServer()
2337 git_host = self._GetGitHost()
2338 assert self._gerrit_server and self._gerrit_host
2339 cookie_auth = gerrit_util.CookiesAuthenticator()
2340
2341 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2342 git_auth = cookie_auth.get_auth_header(git_host)
2343 if gerrit_auth and git_auth:
2344 if gerrit_auth == git_auth:
2345 return
2346 print((
2347 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2348 ' Check your %s or %s file for credentials of hosts:\n'
2349 ' %s\n'
2350 ' %s\n'
2351 ' %s') %
2352 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2353 git_host, self._gerrit_host,
2354 cookie_auth.get_new_password_message(git_host)))
2355 if not force:
2356 ask_for_data('If you know what you are doing, press Enter to continue, '
2357 'Ctrl+C to abort.')
2358 return
2359 else:
2360 missing = (
2361 [] if gerrit_auth else [self._gerrit_host] +
2362 [] if git_auth else [git_host])
2363 DieWithError('Credentials for the following hosts are required:\n'
2364 ' %s\n'
2365 'These are read from %s (or legacy %s)\n'
2366 '%s' % (
2367 '\n '.join(missing),
2368 cookie_auth.get_gitcookies_path(),
2369 cookie_auth.get_netrc_path(),
2370 cookie_auth.get_new_password_message(git_host)))
2371
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002372 def _PostUnsetIssueProperties(self):
2373 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002374 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002375
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002376 def GetRieveldObjForPresubmit(self):
2377 class ThisIsNotRietveldIssue(object):
2378 def __nonzero__(self):
2379 # This is a hack to make presubmit_support think that rietveld is not
2380 # defined, yet still ensure that calls directly result in a decent
2381 # exception message below.
2382 return False
2383
2384 def __getattr__(self, attr):
2385 print(
2386 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2387 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2388 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2389 'or use Rietveld for codereview.\n'
2390 'See also http://crbug.com/579160.' % attr)
2391 raise NotImplementedError()
2392 return ThisIsNotRietveldIssue()
2393
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002394 def GetGerritObjForPresubmit(self):
2395 return presubmit_support.GerritAccessor(self._GetGerritHost())
2396
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002397 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002398 """Apply a rough heuristic to give a simple summary of an issue's review
2399 or CQ status, assuming adherence to a common workflow.
2400
2401 Returns None if no issue for this branch, or one of the following keywords:
2402 * 'error' - error from review tool (including deleted issues)
2403 * 'unsent' - no reviewers added
2404 * 'waiting' - waiting for review
2405 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002406 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2407 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002408 * 'commit' - in the commit queue
2409 * 'closed' - abandoned
2410 """
2411 if not self.GetIssue():
2412 return None
2413
2414 try:
2415 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002416 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002417 return 'error'
2418
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002419 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002420 return 'closed'
2421
2422 cq_label = data['labels'].get('Commit-Queue', {})
2423 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002424 votes = cq_label.get('all', [])
2425 highest_vote = 0
2426 for v in votes:
2427 highest_vote = max(highest_vote, v.get('value', 0))
2428 vote_value = str(highest_vote)
2429 if vote_value != '0':
2430 # Add a '+' if the value is not 0 to match the values in the label.
2431 # The cq_label does not have negatives.
2432 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433 vote_text = cq_label.get('values', {}).get(vote_value, '')
2434 if vote_text.lower() == 'commit':
2435 return 'commit'
2436
2437 lgtm_label = data['labels'].get('Code-Review', {})
2438 if lgtm_label:
2439 if 'rejected' in lgtm_label:
2440 return 'not lgtm'
2441 if 'approved' in lgtm_label:
2442 return 'lgtm'
2443
2444 if not data.get('reviewers', {}).get('REVIEWER', []):
2445 return 'unsent'
2446
2447 messages = data.get('messages', [])
2448 if messages:
2449 owner = data['owner'].get('_account_id')
2450 last_message_author = messages[-1].get('author', {}).get('_account_id')
2451 if owner != last_message_author:
2452 # Some reply from non-owner.
2453 return 'reply'
2454
2455 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002456
2457 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002458 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002459 return data['revisions'][data['current_revision']]['_number']
2460
2461 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002462 data = self._GetChangeDetail(['CURRENT_REVISION'])
2463 current_rev = data['current_revision']
2464 url = data['revisions'][current_rev]['fetch']['http']['url']
2465 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002466
dsansomee2d6fd92016-09-08 00:10:47 -07002467 def UpdateDescriptionRemote(self, description, force=False):
2468 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2469 if not force:
2470 ask_for_data(
2471 'The description cannot be modified while the issue has a pending '
2472 'unpublished edit. Either publish the edit in the Gerrit web UI '
2473 'or delete it.\n\n'
2474 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2475
2476 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2477 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002478 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2479 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002480
2481 def CloseIssue(self):
2482 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2483
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002484 def GetApprovingReviewers(self):
2485 """Returns a list of reviewers approving the change.
2486
2487 Note: not necessarily committers.
2488 """
2489 raise NotImplementedError()
2490
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002491 def SubmitIssue(self, wait_for_merge=True):
2492 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2493 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002494
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002495 def _GetChangeDetail(self, options=None, issue=None):
2496 options = options or []
2497 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002498 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002499 try:
2500 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2501 options, ignore_404=False)
2502 except gerrit_util.GerritError as e:
2503 if e.http_status == 404:
2504 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2505 raise
tandriic2405f52016-10-10 08:13:15 -07002506 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002507
agable32978d92016-11-01 12:55:02 -07002508 def _GetChangeCommit(self, issue=None):
2509 issue = issue or self.GetIssue()
2510 assert issue, 'issue is required to query Gerrit'
2511 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2512 if not data:
2513 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2514 return data
2515
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002516 def CMDLand(self, force, bypass_hooks, verbose):
2517 if git_common.is_dirty_git_tree('land'):
2518 return 1
tandriid60367b2016-06-22 05:25:12 -07002519 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2520 if u'Commit-Queue' in detail.get('labels', {}):
2521 if not force:
2522 ask_for_data('\nIt seems this repository has a Commit Queue, '
2523 'which can test and land changes for you. '
2524 'Are you sure you wish to bypass it?\n'
2525 'Press Enter to continue, Ctrl+C to abort.')
2526
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002527 differs = True
tandriic4344b52016-08-29 06:04:54 -07002528 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002529 # Note: git diff outputs nothing if there is no diff.
2530 if not last_upload or RunGit(['diff', last_upload]).strip():
2531 print('WARNING: some changes from local branch haven\'t been uploaded')
2532 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002533 if detail['current_revision'] == last_upload:
2534 differs = False
2535 else:
2536 print('WARNING: local branch contents differ from latest uploaded '
2537 'patchset')
2538 if differs:
2539 if not force:
2540 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002541 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2542 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002543 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2544 elif not bypass_hooks:
2545 hook_results = self.RunHook(
2546 committing=True,
2547 may_prompt=not force,
2548 verbose=verbose,
2549 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2550 if not hook_results.should_continue():
2551 return 1
2552
2553 self.SubmitIssue(wait_for_merge=True)
2554 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002555 links = self._GetChangeCommit().get('web_links', [])
2556 for link in links:
2557 if link.get('name') == 'gerrit' and link.get('url'):
2558 print('Landed as %s' % link.get('url'))
2559 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002560 return 0
2561
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002562 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2563 directory):
2564 assert not reject
2565 assert not nocommit
2566 assert not directory
2567 assert parsed_issue_arg.valid
2568
2569 self._changelist.issue = parsed_issue_arg.issue
2570
2571 if parsed_issue_arg.hostname:
2572 self._gerrit_host = parsed_issue_arg.hostname
2573 self._gerrit_server = 'https://%s' % self._gerrit_host
2574
tandriic2405f52016-10-10 08:13:15 -07002575 try:
2576 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2577 except GerritIssueNotExists as e:
2578 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002579
2580 if not parsed_issue_arg.patchset:
2581 # Use current revision by default.
2582 revision_info = detail['revisions'][detail['current_revision']]
2583 patchset = int(revision_info['_number'])
2584 else:
2585 patchset = parsed_issue_arg.patchset
2586 for revision_info in detail['revisions'].itervalues():
2587 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2588 break
2589 else:
2590 DieWithError('Couldn\'t find patchset %i in issue %i' %
2591 (parsed_issue_arg.patchset, self.GetIssue()))
2592
2593 fetch_info = revision_info['fetch']['http']
2594 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2595 RunGit(['cherry-pick', 'FETCH_HEAD'])
2596 self.SetIssue(self.GetIssue())
2597 self.SetPatchset(patchset)
2598 print('Committed patch for issue %i pathset %i locally' %
2599 (self.GetIssue(), self.GetPatchset()))
2600 return 0
2601
2602 @staticmethod
2603 def ParseIssueURL(parsed_url):
2604 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2605 return None
2606 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2607 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2608 # Short urls like https://domain/<issue_number> can be used, but don't allow
2609 # specifying the patchset (you'd 404), but we allow that here.
2610 if parsed_url.path == '/':
2611 part = parsed_url.fragment
2612 else:
2613 part = parsed_url.path
2614 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2615 if match:
2616 return _ParsedIssueNumberArgument(
2617 issue=int(match.group(2)),
2618 patchset=int(match.group(4)) if match.group(4) else None,
2619 hostname=parsed_url.netloc)
2620 return None
2621
tandrii16e0b4e2016-06-07 10:34:28 -07002622 def _GerritCommitMsgHookCheck(self, offer_removal):
2623 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2624 if not os.path.exists(hook):
2625 return
2626 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2627 # custom developer made one.
2628 data = gclient_utils.FileRead(hook)
2629 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2630 return
2631 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002632 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002633 'and may interfere with it in subtle ways.\n'
2634 'We recommend you remove the commit-msg hook.')
2635 if offer_removal:
2636 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2637 if reply.lower().startswith('y'):
2638 gclient_utils.rm_file_or_tree(hook)
2639 print('Gerrit commit-msg hook removed.')
2640 else:
2641 print('OK, will keep Gerrit commit-msg hook in place.')
2642
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002643 def CMDUploadChange(self, options, args, change):
2644 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002645 if options.squash and options.no_squash:
2646 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002647
2648 if not options.squash and not options.no_squash:
2649 # Load default for user, repo, squash=true, in this order.
2650 options.squash = settings.GetSquashGerritUploads()
2651 elif options.no_squash:
2652 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002653
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002654 # We assume the remote called "origin" is the one we want.
2655 # It is probably not worthwhile to support different workflows.
2656 gerrit_remote = 'origin'
2657
2658 remote, remote_branch = self.GetRemoteBranch()
2659 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2660 pending_prefix='')
2661
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002663 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002664 if self.GetIssue():
2665 # Try to get the message from a previous upload.
2666 message = self.GetDescription()
2667 if not message:
2668 DieWithError(
2669 'failed to fetch description from current Gerrit issue %d\n'
2670 '%s' % (self.GetIssue(), self.GetIssueURL()))
2671 change_id = self._GetChangeDetail()['change_id']
2672 while True:
2673 footer_change_ids = git_footers.get_footer_change_id(message)
2674 if footer_change_ids == [change_id]:
2675 break
2676 if not footer_change_ids:
2677 message = git_footers.add_footer_change_id(message, change_id)
2678 print('WARNING: appended missing Change-Id to issue description')
2679 continue
2680 # There is already a valid footer but with different or several ids.
2681 # Doing this automatically is non-trivial as we don't want to lose
2682 # existing other footers, yet we want to append just 1 desired
2683 # Change-Id. Thus, just create a new footer, but let user verify the
2684 # new description.
2685 message = '%s\n\nChange-Id: %s' % (message, change_id)
2686 print(
2687 'WARNING: issue %s has Change-Id footer(s):\n'
2688 ' %s\n'
2689 'but issue has Change-Id %s, according to Gerrit.\n'
2690 'Please, check the proposed correction to the description, '
2691 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2692 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2693 change_id))
2694 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2695 if not options.force:
2696 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002697 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002698 message = change_desc.description
2699 if not message:
2700 DieWithError("Description is empty. Aborting...")
2701 # Continue the while loop.
2702 # Sanity check of this code - we should end up with proper message
2703 # footer.
2704 assert [change_id] == git_footers.get_footer_change_id(message)
2705 change_desc = ChangeDescription(message)
2706 else:
2707 change_desc = ChangeDescription(
2708 options.message or CreateDescriptionFromLog(args))
2709 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002710 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002711 if not change_desc.description:
2712 DieWithError("Description is empty. Aborting...")
2713 message = change_desc.description
2714 change_ids = git_footers.get_footer_change_id(message)
2715 if len(change_ids) > 1:
2716 DieWithError('too many Change-Id footers, at most 1 allowed.')
2717 if not change_ids:
2718 # Generate the Change-Id automatically.
2719 message = git_footers.add_footer_change_id(
2720 message, GenerateGerritChangeId(message))
2721 change_desc.set_description(message)
2722 change_ids = git_footers.get_footer_change_id(message)
2723 assert len(change_ids) == 1
2724 change_id = change_ids[0]
2725
2726 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2727 if remote is '.':
2728 # If our upstream branch is local, we base our squashed commit on its
2729 # squashed version.
2730 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2731 # Check the squashed hash of the parent.
2732 parent = RunGit(['config',
2733 'branch.%s.gerritsquashhash' % upstream_branch_name],
2734 error_ok=True).strip()
2735 # Verify that the upstream branch has been uploaded too, otherwise
2736 # Gerrit will create additional CLs when uploading.
2737 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2738 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 DieWithError(
2740 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002741 'Note: maybe you\'ve uploaded it with --no-squash. '
2742 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 ' git cl upload --squash\n' % upstream_branch_name)
2744 else:
2745 parent = self.GetCommonAncestorWithUpstream()
2746
2747 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2748 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2749 '-m', message]).strip()
2750 else:
2751 change_desc = ChangeDescription(
2752 options.message or CreateDescriptionFromLog(args))
2753 if not change_desc.description:
2754 DieWithError("Description is empty. Aborting...")
2755
2756 if not git_footers.get_footer_change_id(change_desc.description):
2757 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002758 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2759 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002760 ref_to_push = 'HEAD'
2761 parent = '%s/%s' % (gerrit_remote, branch)
2762 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2763
2764 assert change_desc
2765 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2766 ref_to_push)]).splitlines()
2767 if len(commits) > 1:
2768 print('WARNING: This will upload %d commits. Run the following command '
2769 'to see which commits will be uploaded: ' % len(commits))
2770 print('git log %s..%s' % (parent, ref_to_push))
2771 print('You can also use `git squash-branch` to squash these into a '
2772 'single commit.')
2773 ask_for_data('About to upload; enter to confirm.')
2774
2775 if options.reviewers or options.tbr_owners:
2776 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2777 change)
2778
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002779 # Extra options that can be specified at push time. Doc:
2780 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2781 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002782 if change_desc.get_reviewers(tbr_only=True):
2783 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2784 refspec_opts.append('l=Code-Review+1')
2785
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002786 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002787 if not re.match(r'^[\w ]+$', options.title):
2788 options.title = re.sub(r'[^\w ]', '', options.title)
2789 print('WARNING: Patchset title may only contain alphanumeric chars '
2790 'and spaces. Cleaned up title:\n%s' % options.title)
2791 if not options.force:
2792 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002793 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2794 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002795 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2796
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002797 if options.send_mail:
2798 if not change_desc.get_reviewers():
2799 DieWithError('Must specify reviewers to send email.')
2800 refspec_opts.append('notify=ALL')
2801 else:
2802 refspec_opts.append('notify=NONE')
2803
tandrii99a72f22016-08-17 14:33:24 -07002804 reviewers = change_desc.get_reviewers()
2805 if reviewers:
2806 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002807
agablec6787972016-09-09 16:13:34 -07002808 if options.private:
2809 refspec_opts.append('draft')
2810
rmistry9eadede2016-09-19 11:22:43 -07002811 if options.topic:
2812 # Documentation on Gerrit topics is here:
2813 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2814 refspec_opts.append('topic=%s' % options.topic)
2815
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002816 refspec_suffix = ''
2817 if refspec_opts:
2818 refspec_suffix = '%' + ','.join(refspec_opts)
2819 assert ' ' not in refspec_suffix, (
2820 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002821 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002822
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002824 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002825 print_stdout=True,
2826 # Flush after every line: useful for seeing progress when running as
2827 # recipe.
2828 filter_fn=lambda _: sys.stdout.flush())
2829
2830 if options.squash:
2831 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2832 change_numbers = [m.group(1)
2833 for m in map(regex.match, push_stdout.splitlines())
2834 if m]
2835 if len(change_numbers) != 1:
2836 DieWithError(
2837 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2838 'Change-Id: %s') % (len(change_numbers), change_id))
2839 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002840 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002841
2842 # Add cc's from the CC_LIST and --cc flag (if any).
2843 cc = self.GetCCList().split(',')
2844 if options.cc:
2845 cc.extend(options.cc)
2846 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002847 if change_desc.get_cced():
2848 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002849 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002850 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07002851 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 return 0
2853
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002854 def _AddChangeIdToCommitMessage(self, options, args):
2855 """Re-commits using the current message, assumes the commit hook is in
2856 place.
2857 """
2858 log_desc = options.message or CreateDescriptionFromLog(args)
2859 git_command = ['commit', '--amend', '-m', log_desc]
2860 RunGit(git_command)
2861 new_log_desc = CreateDescriptionFromLog(args)
2862 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002863 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002864 return new_log_desc
2865 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002866 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002867
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002868 def SetCQState(self, new_state):
2869 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002870 vote_map = {
2871 _CQState.NONE: 0,
2872 _CQState.DRY_RUN: 1,
2873 _CQState.COMMIT : 2,
2874 }
2875 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2876 labels={'Commit-Queue': vote_map[new_state]})
2877
tandriie113dfd2016-10-11 10:20:12 -07002878 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002879 try:
2880 data = self._GetChangeDetail()
2881 except GerritIssueNotExists:
2882 return 'Gerrit doesn\'t know about your issue %s' % self.GetIssue()
2883
2884 if data['status'] in ('ABANDONED', 'MERGED'):
2885 return 'CL %s is closed' % self.GetIssue()
2886
2887 def GetTryjobProperties(self, patchset=None):
2888 """Returns dictionary of properties to launch tryjob."""
2889 data = self._GetChangeDetail(['ALL_REVISIONS'])
2890 patchset = int(patchset or self.GetPatchset())
2891 assert patchset
2892 revision_data = None # Pylint wants it to be defined.
2893 for revision_data in data['revisions'].itervalues():
2894 if int(revision_data['_number']) == patchset:
2895 break
2896 else:
2897 raise Exception('Patchset %d is not known in Gerrit issue %d' %
2898 (patchset, self.GetIssue()))
2899 return {
2900 'patch_issue': self.GetIssue(),
2901 'patch_set': patchset or self.GetPatchset(),
2902 'patch_project': data['project'],
2903 'patch_storage': 'gerrit',
2904 'patch_ref': revision_data['fetch']['http']['ref'],
2905 'patch_repository_url': revision_data['fetch']['http']['url'],
2906 'patch_gerrit_url': self.GetCodereviewServer(),
2907 }
tandriie113dfd2016-10-11 10:20:12 -07002908
tandriide281ae2016-10-12 06:02:30 -07002909 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002910 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002911
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002912
2913_CODEREVIEW_IMPLEMENTATIONS = {
2914 'rietveld': _RietveldChangelistImpl,
2915 'gerrit': _GerritChangelistImpl,
2916}
2917
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002918
iannuccie53c9352016-08-17 14:40:40 -07002919def _add_codereview_issue_select_options(parser, extra=""):
2920 _add_codereview_select_options(parser)
2921
2922 text = ('Operate on this issue number instead of the current branch\'s '
2923 'implicit issue.')
2924 if extra:
2925 text += ' '+extra
2926 parser.add_option('-i', '--issue', type=int, help=text)
2927
2928
2929def _process_codereview_issue_select_options(parser, options):
2930 _process_codereview_select_options(parser, options)
2931 if options.issue is not None and not options.forced_codereview:
2932 parser.error('--issue must be specified with either --rietveld or --gerrit')
2933
2934
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002935def _add_codereview_select_options(parser):
2936 """Appends --gerrit and --rietveld options to force specific codereview."""
2937 parser.codereview_group = optparse.OptionGroup(
2938 parser, 'EXPERIMENTAL! Codereview override options')
2939 parser.add_option_group(parser.codereview_group)
2940 parser.codereview_group.add_option(
2941 '--gerrit', action='store_true',
2942 help='Force the use of Gerrit for codereview')
2943 parser.codereview_group.add_option(
2944 '--rietveld', action='store_true',
2945 help='Force the use of Rietveld for codereview')
2946
2947
2948def _process_codereview_select_options(parser, options):
2949 if options.gerrit and options.rietveld:
2950 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2951 options.forced_codereview = None
2952 if options.gerrit:
2953 options.forced_codereview = 'gerrit'
2954 elif options.rietveld:
2955 options.forced_codereview = 'rietveld'
2956
2957
tandriif9aefb72016-07-01 09:06:51 -07002958def _get_bug_line_values(default_project, bugs):
2959 """Given default_project and comma separated list of bugs, yields bug line
2960 values.
2961
2962 Each bug can be either:
2963 * a number, which is combined with default_project
2964 * string, which is left as is.
2965
2966 This function may produce more than one line, because bugdroid expects one
2967 project per line.
2968
2969 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2970 ['v8:123', 'chromium:789']
2971 """
2972 default_bugs = []
2973 others = []
2974 for bug in bugs.split(','):
2975 bug = bug.strip()
2976 if bug:
2977 try:
2978 default_bugs.append(int(bug))
2979 except ValueError:
2980 others.append(bug)
2981
2982 if default_bugs:
2983 default_bugs = ','.join(map(str, default_bugs))
2984 if default_project:
2985 yield '%s:%s' % (default_project, default_bugs)
2986 else:
2987 yield default_bugs
2988 for other in sorted(others):
2989 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2990 yield other
2991
2992
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002993class ChangeDescription(object):
2994 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002995 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002996 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002997 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002998
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002999 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003000 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003001
agable@chromium.org42c20792013-09-12 17:34:49 +00003002 @property # www.logilab.org/ticket/89786
3003 def description(self): # pylint: disable=E0202
3004 return '\n'.join(self._description_lines)
3005
3006 def set_description(self, desc):
3007 if isinstance(desc, basestring):
3008 lines = desc.splitlines()
3009 else:
3010 lines = [line.rstrip() for line in desc]
3011 while lines and not lines[0]:
3012 lines.pop(0)
3013 while lines and not lines[-1]:
3014 lines.pop(-1)
3015 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003016
piman@chromium.org336f9122014-09-04 02:16:55 +00003017 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003018 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003019 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003020 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003022 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003023
agable@chromium.org42c20792013-09-12 17:34:49 +00003024 # Get the set of R= and TBR= lines and remove them from the desciption.
3025 regexp = re.compile(self.R_LINE)
3026 matches = [regexp.match(line) for line in self._description_lines]
3027 new_desc = [l for i, l in enumerate(self._description_lines)
3028 if not matches[i]]
3029 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003030
agable@chromium.org42c20792013-09-12 17:34:49 +00003031 # Construct new unified R= and TBR= lines.
3032 r_names = []
3033 tbr_names = []
3034 for match in matches:
3035 if not match:
3036 continue
3037 people = cleanup_list([match.group(2).strip()])
3038 if match.group(1) == 'TBR':
3039 tbr_names.extend(people)
3040 else:
3041 r_names.extend(people)
3042 for name in r_names:
3043 if name not in reviewers:
3044 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003045 if add_owners_tbr:
3046 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003047 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003048 all_reviewers = set(tbr_names + reviewers)
3049 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3050 all_reviewers)
3051 tbr_names.extend(owners_db.reviewers_for(missing_files,
3052 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003053 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3054 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3055
3056 # Put the new lines in the description where the old first R= line was.
3057 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3058 if 0 <= line_loc < len(self._description_lines):
3059 if new_tbr_line:
3060 self._description_lines.insert(line_loc, new_tbr_line)
3061 if new_r_line:
3062 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003063 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 if new_r_line:
3065 self.append_footer(new_r_line)
3066 if new_tbr_line:
3067 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003068
tandriif9aefb72016-07-01 09:06:51 -07003069 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003070 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003071 self.set_description([
3072 '# Enter a description of the change.',
3073 '# This will be displayed on the codereview site.',
3074 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003075 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 '--------------------',
3077 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078
agable@chromium.org42c20792013-09-12 17:34:49 +00003079 regexp = re.compile(self.BUG_LINE)
3080 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003081 prefix = settings.GetBugPrefix()
3082 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3083 for value in values:
3084 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3085 self.append_footer('BUG=%s' % value)
3086
agable@chromium.org42c20792013-09-12 17:34:49 +00003087 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003088 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003089 if not content:
3090 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003091 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092
3093 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3095 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003096 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003097 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003098
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003099 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003100 """Adds a footer line to the description.
3101
3102 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3103 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3104 that Gerrit footers are always at the end.
3105 """
3106 parsed_footer_line = git_footers.parse_footer(line)
3107 if parsed_footer_line:
3108 # Line is a gerrit footer in the form: Footer-Key: any value.
3109 # Thus, must be appended observing Gerrit footer rules.
3110 self.set_description(
3111 git_footers.add_footer(self.description,
3112 key=parsed_footer_line[0],
3113 value=parsed_footer_line[1]))
3114 return
3115
3116 if not self._description_lines:
3117 self._description_lines.append(line)
3118 return
3119
3120 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3121 if gerrit_footers:
3122 # git_footers.split_footers ensures that there is an empty line before
3123 # actual (gerrit) footers, if any. We have to keep it that way.
3124 assert top_lines and top_lines[-1] == ''
3125 top_lines, separator = top_lines[:-1], top_lines[-1:]
3126 else:
3127 separator = [] # No need for separator if there are no gerrit_footers.
3128
3129 prev_line = top_lines[-1] if top_lines else ''
3130 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3131 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3132 top_lines.append('')
3133 top_lines.append(line)
3134 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003135
tandrii99a72f22016-08-17 14:33:24 -07003136 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003137 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003138 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003139 reviewers = [match.group(2).strip()
3140 for match in matches
3141 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003142 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003143
bradnelsond975b302016-10-23 12:20:23 -07003144 def get_cced(self):
3145 """Retrieves the list of reviewers."""
3146 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3147 cced = [match.group(2).strip() for match in matches if match]
3148 return cleanup_list(cced)
3149
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003150
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003151def get_approving_reviewers(props):
3152 """Retrieves the reviewers that approved a CL from the issue properties with
3153 messages.
3154
3155 Note that the list may contain reviewers that are not committer, thus are not
3156 considered by the CQ.
3157 """
3158 return sorted(
3159 set(
3160 message['sender']
3161 for message in props['messages']
3162 if message['approval'] and message['sender'] in props['reviewers']
3163 )
3164 )
3165
3166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003167def FindCodereviewSettingsFile(filename='codereview.settings'):
3168 """Finds the given file starting in the cwd and going up.
3169
3170 Only looks up to the top of the repository unless an
3171 'inherit-review-settings-ok' file exists in the root of the repository.
3172 """
3173 inherit_ok_file = 'inherit-review-settings-ok'
3174 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003175 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003176 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3177 root = '/'
3178 while True:
3179 if filename in os.listdir(cwd):
3180 if os.path.isfile(os.path.join(cwd, filename)):
3181 return open(os.path.join(cwd, filename))
3182 if cwd == root:
3183 break
3184 cwd = os.path.dirname(cwd)
3185
3186
3187def LoadCodereviewSettingsFromFile(fileobj):
3188 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003189 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191 def SetProperty(name, setting, unset_error_ok=False):
3192 fullname = 'rietveld.' + name
3193 if setting in keyvals:
3194 RunGit(['config', fullname, keyvals[setting]])
3195 else:
3196 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3197
tandrii48df5812016-10-17 03:55:37 -07003198 if not keyvals.get('GERRIT_HOST', False):
3199 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003200 # Only server setting is required. Other settings can be absent.
3201 # In that case, we ignore errors raised during option deletion attempt.
3202 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003203 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003204 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3205 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003206 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003207 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003208 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3209 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003210 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003211 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003212 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003213 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003214 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3215 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003216
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003217 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003218 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003219
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003220 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003221 RunGit(['config', 'gerrit.squash-uploads',
3222 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003223
tandrii@chromium.org28253532016-04-14 13:46:56 +00003224 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003225 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003226 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3229 #should be of the form
3230 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3231 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3232 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3233 keyvals['ORIGIN_URL_CONFIG']])
3234
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003236def urlretrieve(source, destination):
3237 """urllib is broken for SSL connections via a proxy therefore we
3238 can't use urllib.urlretrieve()."""
3239 with open(destination, 'w') as f:
3240 f.write(urllib2.urlopen(source).read())
3241
3242
ukai@chromium.org712d6102013-11-27 00:52:58 +00003243def hasSheBang(fname):
3244 """Checks fname is a #! script."""
3245 with open(fname) as f:
3246 return f.read(2).startswith('#!')
3247
3248
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003249# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3250def DownloadHooks(*args, **kwargs):
3251 pass
3252
3253
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003254def DownloadGerritHook(force):
3255 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003256
3257 Args:
3258 force: True to update hooks. False to install hooks if not present.
3259 """
3260 if not settings.GetIsGerrit():
3261 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003262 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003263 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3264 if not os.access(dst, os.X_OK):
3265 if os.path.exists(dst):
3266 if not force:
3267 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003268 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003269 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003270 if not hasSheBang(dst):
3271 DieWithError('Not a script: %s\n'
3272 'You need to download from\n%s\n'
3273 'into .git/hooks/commit-msg and '
3274 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003275 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3276 except Exception:
3277 if os.path.exists(dst):
3278 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003279 DieWithError('\nFailed to download hooks.\n'
3280 'You need to download from\n%s\n'
3281 'into .git/hooks/commit-msg and '
3282 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003283
3284
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003285
3286def GetRietveldCodereviewSettingsInteractively():
3287 """Prompt the user for settings."""
3288 server = settings.GetDefaultServerUrl(error_ok=True)
3289 prompt = 'Rietveld server (host[:port])'
3290 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3291 newserver = ask_for_data(prompt + ':')
3292 if not server and not newserver:
3293 newserver = DEFAULT_SERVER
3294 if newserver:
3295 newserver = gclient_utils.UpgradeToHttps(newserver)
3296 if newserver != server:
3297 RunGit(['config', 'rietveld.server', newserver])
3298
3299 def SetProperty(initial, caption, name, is_url):
3300 prompt = caption
3301 if initial:
3302 prompt += ' ("x" to clear) [%s]' % initial
3303 new_val = ask_for_data(prompt + ':')
3304 if new_val == 'x':
3305 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3306 elif new_val:
3307 if is_url:
3308 new_val = gclient_utils.UpgradeToHttps(new_val)
3309 if new_val != initial:
3310 RunGit(['config', 'rietveld.' + name, new_val])
3311
3312 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3313 SetProperty(settings.GetDefaultPrivateFlag(),
3314 'Private flag (rietveld only)', 'private', False)
3315 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3316 'tree-status-url', False)
3317 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3318 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3319 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3320 'run-post-upload-hook', False)
3321
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003322@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003323def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003324 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003325
tandrii5d0a0422016-09-14 06:24:35 -07003326 print('WARNING: git cl config works for Rietveld only')
3327 # TODO(tandrii): remove this once we switch to Gerrit.
3328 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003329 parser.add_option('--activate-update', action='store_true',
3330 help='activate auto-updating [rietveld] section in '
3331 '.git/config')
3332 parser.add_option('--deactivate-update', action='store_true',
3333 help='deactivate auto-updating [rietveld] section in '
3334 '.git/config')
3335 options, args = parser.parse_args(args)
3336
3337 if options.deactivate_update:
3338 RunGit(['config', 'rietveld.autoupdate', 'false'])
3339 return
3340
3341 if options.activate_update:
3342 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3343 return
3344
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003346 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003347 return 0
3348
3349 url = args[0]
3350 if not url.endswith('codereview.settings'):
3351 url = os.path.join(url, 'codereview.settings')
3352
3353 # Load code review settings and download hooks (if available).
3354 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3355 return 0
3356
3357
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003358def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003359 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003360 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3361 branch = ShortBranchName(branchref)
3362 _, args = parser.parse_args(args)
3363 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003364 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003365 return RunGit(['config', 'branch.%s.base-url' % branch],
3366 error_ok=False).strip()
3367 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003368 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003369 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3370 error_ok=False).strip()
3371
3372
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003373def color_for_status(status):
3374 """Maps a Changelist status to color, for CMDstatus and other tools."""
3375 return {
3376 'unsent': Fore.RED,
3377 'waiting': Fore.BLUE,
3378 'reply': Fore.YELLOW,
3379 'lgtm': Fore.GREEN,
3380 'commit': Fore.MAGENTA,
3381 'closed': Fore.CYAN,
3382 'error': Fore.WHITE,
3383 }.get(status, Fore.WHITE)
3384
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003385
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003386def get_cl_statuses(changes, fine_grained, max_processes=None):
3387 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003388
3389 If fine_grained is true, this will fetch CL statuses from the server.
3390 Otherwise, simply indicate if there's a matching url for the given branches.
3391
3392 If max_processes is specified, it is used as the maximum number of processes
3393 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3394 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003395
3396 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003397 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003398 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003399 upload.verbosity = 0
3400
3401 if fine_grained:
3402 # Process one branch synchronously to work through authentication, then
3403 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003404 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003405 def fetch(cl):
3406 try:
3407 return (cl, cl.GetStatus())
3408 except:
3409 # See http://crbug.com/629863.
3410 logging.exception('failed to fetch status for %s:', cl)
3411 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003412 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003413
tandriiea9514a2016-08-17 12:32:37 -07003414 changes_to_fetch = changes[1:]
3415 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003416 # Exit early if there was only one branch to fetch.
3417 return
3418
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003419 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003420 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003421 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003422 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003423
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003424 fetched_cls = set()
3425 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003426 while True:
3427 try:
3428 row = it.next(timeout=5)
3429 except multiprocessing.TimeoutError:
3430 break
3431
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003432 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003433 yield row
3434
3435 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003436 for cl in set(changes_to_fetch) - fetched_cls:
3437 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003438
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003439 else:
3440 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003441 for cl in changes:
3442 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003443
rmistry@google.com2dd99862015-06-22 12:22:18 +00003444
3445def upload_branch_deps(cl, args):
3446 """Uploads CLs of local branches that are dependents of the current branch.
3447
3448 If the local branch dependency tree looks like:
3449 test1 -> test2.1 -> test3.1
3450 -> test3.2
3451 -> test2.2 -> test3.3
3452
3453 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3454 run on the dependent branches in this order:
3455 test2.1, test3.1, test3.2, test2.2, test3.3
3456
3457 Note: This function does not rebase your local dependent branches. Use it when
3458 you make a change to the parent branch that will not conflict with its
3459 dependent branches, and you would like their dependencies updated in
3460 Rietveld.
3461 """
3462 if git_common.is_dirty_git_tree('upload-branch-deps'):
3463 return 1
3464
3465 root_branch = cl.GetBranch()
3466 if root_branch is None:
3467 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3468 'Get on a branch!')
3469 if not cl.GetIssue() or not cl.GetPatchset():
3470 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3471 'patchset dependencies without an uploaded CL.')
3472
3473 branches = RunGit(['for-each-ref',
3474 '--format=%(refname:short) %(upstream:short)',
3475 'refs/heads'])
3476 if not branches:
3477 print('No local branches found.')
3478 return 0
3479
3480 # Create a dictionary of all local branches to the branches that are dependent
3481 # on it.
3482 tracked_to_dependents = collections.defaultdict(list)
3483 for b in branches.splitlines():
3484 tokens = b.split()
3485 if len(tokens) == 2:
3486 branch_name, tracked = tokens
3487 tracked_to_dependents[tracked].append(branch_name)
3488
vapiera7fbd5a2016-06-16 09:17:49 -07003489 print()
3490 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003491 dependents = []
3492 def traverse_dependents_preorder(branch, padding=''):
3493 dependents_to_process = tracked_to_dependents.get(branch, [])
3494 padding += ' '
3495 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003496 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003497 dependents.append(dependent)
3498 traverse_dependents_preorder(dependent, padding)
3499 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003500 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003501
3502 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003503 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003504 return 0
3505
vapiera7fbd5a2016-06-16 09:17:49 -07003506 print('This command will checkout all dependent branches and run '
3507 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003508 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3509
andybons@chromium.org962f9462016-02-03 20:00:42 +00003510 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003511 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003512 args.extend(['-t', 'Updated patchset dependency'])
3513
rmistry@google.com2dd99862015-06-22 12:22:18 +00003514 # Record all dependents that failed to upload.
3515 failures = {}
3516 # Go through all dependents, checkout the branch and upload.
3517 try:
3518 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003519 print()
3520 print('--------------------------------------')
3521 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003522 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003523 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003524 try:
3525 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003526 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003527 failures[dependent_branch] = 1
3528 except: # pylint: disable=W0702
3529 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003530 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003531 finally:
3532 # Swap back to the original root branch.
3533 RunGit(['checkout', '-q', root_branch])
3534
vapiera7fbd5a2016-06-16 09:17:49 -07003535 print()
3536 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003537 for dependent_branch in dependents:
3538 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003539 print(' %s : %s' % (dependent_branch, upload_status))
3540 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003541
3542 return 0
3543
3544
kmarshall3bff56b2016-06-06 18:31:47 -07003545def CMDarchive(parser, args):
3546 """Archives and deletes branches associated with closed changelists."""
3547 parser.add_option(
3548 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003549 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003550 parser.add_option(
3551 '-f', '--force', action='store_true',
3552 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003553 parser.add_option(
3554 '-d', '--dry-run', action='store_true',
3555 help='Skip the branch tagging and removal steps.')
3556 parser.add_option(
3557 '-t', '--notags', action='store_true',
3558 help='Do not tag archived branches. '
3559 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003560
3561 auth.add_auth_options(parser)
3562 options, args = parser.parse_args(args)
3563 if args:
3564 parser.error('Unsupported args: %s' % ' '.join(args))
3565 auth_config = auth.extract_auth_config_from_options(options)
3566
3567 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3568 if not branches:
3569 return 0
3570
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003572 changes = [Changelist(branchref=b, auth_config=auth_config)
3573 for b in branches.splitlines()]
3574 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3575 statuses = get_cl_statuses(changes,
3576 fine_grained=True,
3577 max_processes=options.maxjobs)
3578 proposal = [(cl.GetBranch(),
3579 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3580 for cl, status in statuses
3581 if status == 'closed']
3582 proposal.sort()
3583
3584 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003585 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003586 return 0
3587
3588 current_branch = GetCurrentBranch()
3589
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003591 if options.notags:
3592 for next_item in proposal:
3593 print(' ' + next_item[0])
3594 else:
3595 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3596 for next_item in proposal:
3597 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003598
kmarshall9249e012016-08-23 12:02:16 -07003599 # Quit now on precondition failure or if instructed by the user, either
3600 # via an interactive prompt or by command line flags.
3601 if options.dry_run:
3602 print('\nNo changes were made (dry run).\n')
3603 return 0
3604 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003605 print('You are currently on a branch \'%s\' which is associated with a '
3606 'closed codereview issue, so archive cannot proceed. Please '
3607 'checkout another branch and run this command again.' %
3608 current_branch)
3609 return 1
kmarshall9249e012016-08-23 12:02:16 -07003610 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003611 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3612 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003613 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003614 return 1
3615
3616 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003617 if not options.notags:
3618 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003619 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003620
vapiera7fbd5a2016-06-16 09:17:49 -07003621 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003622
3623 return 0
3624
3625
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003626def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003627 """Show status of changelists.
3628
3629 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003630 - Red not sent for review or broken
3631 - Blue waiting for review
3632 - Yellow waiting for you to reply to review
3633 - Green LGTM'ed
3634 - Magenta in the commit queue
3635 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003636
3637 Also see 'git cl comments'.
3638 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003639 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003640 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003641 parser.add_option('-f', '--fast', action='store_true',
3642 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003643 parser.add_option(
3644 '-j', '--maxjobs', action='store', type=int,
3645 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003646
3647 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003648 _add_codereview_issue_select_options(
3649 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003650 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003651 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003652 if args:
3653 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003654 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003655
iannuccie53c9352016-08-17 14:40:40 -07003656 if options.issue is not None and not options.field:
3657 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003660 cl = Changelist(auth_config=auth_config, issue=options.issue,
3661 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003663 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664 elif options.field == 'id':
3665 issueid = cl.GetIssue()
3666 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003667 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003668 elif options.field == 'patch':
3669 patchset = cl.GetPatchset()
3670 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003671 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003672 elif options.field == 'status':
3673 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003674 elif options.field == 'url':
3675 url = cl.GetIssueURL()
3676 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003678 return 0
3679
3680 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3681 if not branches:
3682 print('No local branch found.')
3683 return 0
3684
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003685 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003686 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003687 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003688 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003689 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003690 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003691 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003692
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003693 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003694 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3695 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3696 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003697 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003698 c, status = output.next()
3699 branch_statuses[c.GetBranch()] = status
3700 status = branch_statuses.pop(branch)
3701 url = cl.GetIssueURL()
3702 if url and (not status or status == 'error'):
3703 # The issue probably doesn't exist anymore.
3704 url += ' (broken)'
3705
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003706 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003707 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003708 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003709 color = ''
3710 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003711 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003713 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003714 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003715
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003716 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print()
3718 print('Current branch:',)
3719 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003720 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003722 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003724 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003725 print('Issue description:')
3726 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003727 return 0
3728
3729
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003730def colorize_CMDstatus_doc():
3731 """To be called once in main() to add colors to git cl status help."""
3732 colors = [i for i in dir(Fore) if i[0].isupper()]
3733
3734 def colorize_line(line):
3735 for color in colors:
3736 if color in line.upper():
3737 # Extract whitespaces first and the leading '-'.
3738 indent = len(line) - len(line.lstrip(' ')) + 1
3739 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3740 return line
3741
3742 lines = CMDstatus.__doc__.splitlines()
3743 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3744
3745
phajdan.jre328cf92016-08-22 04:12:17 -07003746def write_json(path, contents):
3747 with open(path, 'w') as f:
3748 json.dump(contents, f)
3749
3750
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003751@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003752def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003753 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003754
3755 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003756 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003757 parser.add_option('-r', '--reverse', action='store_true',
3758 help='Lookup the branch(es) for the specified issues. If '
3759 'no issues are specified, all branches with mapped '
3760 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003761 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003762 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003763 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003764 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003765
dnj@chromium.org406c4402015-03-03 17:22:28 +00003766 if options.reverse:
3767 branches = RunGit(['for-each-ref', 'refs/heads',
3768 '--format=%(refname:short)']).splitlines()
3769
3770 # Reverse issue lookup.
3771 issue_branch_map = {}
3772 for branch in branches:
3773 cl = Changelist(branchref=branch)
3774 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3775 if not args:
3776 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003777 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003778 for issue in args:
3779 if not issue:
3780 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003781 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003782 print('Branch for issue number %s: %s' % (
3783 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003784 if options.json:
3785 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003786 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003787 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003788 if len(args) > 0:
3789 try:
3790 issue = int(args[0])
3791 except ValueError:
3792 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003793 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003794 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003795 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003796 if options.json:
3797 write_json(options.json, {
3798 'issue': cl.GetIssue(),
3799 'issue_url': cl.GetIssueURL(),
3800 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003801 return 0
3802
3803
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003804def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003805 """Shows or posts review comments for any changelist."""
3806 parser.add_option('-a', '--add-comment', dest='comment',
3807 help='comment to add to an issue')
3808 parser.add_option('-i', dest='issue',
3809 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003810 parser.add_option('-j', '--json-file',
3811 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003812 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003813 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003814 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003815
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003816 issue = None
3817 if options.issue:
3818 try:
3819 issue = int(options.issue)
3820 except ValueError:
3821 DieWithError('A review issue id is expected to be a number')
3822
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003823 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003824
3825 if options.comment:
3826 cl.AddComment(options.comment)
3827 return 0
3828
3829 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003830 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003831 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003832 summary.append({
3833 'date': message['date'],
3834 'lgtm': False,
3835 'message': message['text'],
3836 'not_lgtm': False,
3837 'sender': message['sender'],
3838 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003839 if message['disapproval']:
3840 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003841 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003842 elif message['approval']:
3843 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003844 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003845 elif message['sender'] == data['owner_email']:
3846 color = Fore.MAGENTA
3847 else:
3848 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003849 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003850 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003851 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003852 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003854 if options.json_file:
3855 with open(options.json_file, 'wb') as f:
3856 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003857 return 0
3858
3859
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003860@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003861def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003862 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003863 parser.add_option('-d', '--display', action='store_true',
3864 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003865 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003866 help='New description to set for this issue (- for stdin, '
3867 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003868 parser.add_option('-f', '--force', action='store_true',
3869 help='Delete any unpublished Gerrit edits for this issue '
3870 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003871
3872 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003873 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003874 options, args = parser.parse_args(args)
3875 _process_codereview_select_options(parser, options)
3876
3877 target_issue = None
3878 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003879 target_issue = ParseIssueNumberArgument(args[0])
3880 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003881 parser.print_help()
3882 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003883
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003884 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003885
martiniss6eda05f2016-06-30 10:18:35 -07003886 kwargs = {
3887 'auth_config': auth_config,
3888 'codereview': options.forced_codereview,
3889 }
3890 if target_issue:
3891 kwargs['issue'] = target_issue.issue
3892 if options.forced_codereview == 'rietveld':
3893 kwargs['rietveld_server'] = target_issue.hostname
3894
3895 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003896
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003897 if not cl.GetIssue():
3898 DieWithError('This branch has no associated changelist.')
3899 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003900
smut@google.com34fb6b12015-07-13 20:03:26 +00003901 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003902 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003903 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003904
3905 if options.new_description:
3906 text = options.new_description
3907 if text == '-':
3908 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003909 elif text == '+':
3910 base_branch = cl.GetCommonAncestorWithUpstream()
3911 change = cl.GetChange(base_branch, None, local_description=True)
3912 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003913
3914 description.set_description(text)
3915 else:
3916 description.prompt()
3917
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003918 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003919 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003920 return 0
3921
3922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923def CreateDescriptionFromLog(args):
3924 """Pulls out the commit log to use as a base for the CL description."""
3925 log_args = []
3926 if len(args) == 1 and not args[0].endswith('.'):
3927 log_args = [args[0] + '..']
3928 elif len(args) == 1 and args[0].endswith('...'):
3929 log_args = [args[0][:-1]]
3930 elif len(args) == 2:
3931 log_args = [args[0] + '..' + args[1]]
3932 else:
3933 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003934 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003935
3936
thestig@chromium.org44202a22014-03-11 19:22:18 +00003937def CMDlint(parser, args):
3938 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003939 parser.add_option('--filter', action='append', metavar='-x,+y',
3940 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003941 auth.add_auth_options(parser)
3942 options, args = parser.parse_args(args)
3943 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003944
3945 # Access to a protected member _XX of a client class
3946 # pylint: disable=W0212
3947 try:
3948 import cpplint
3949 import cpplint_chromium
3950 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003951 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003952 return 1
3953
3954 # Change the current working directory before calling lint so that it
3955 # shows the correct base.
3956 previous_cwd = os.getcwd()
3957 os.chdir(settings.GetRoot())
3958 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003959 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003960 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3961 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003962 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003963 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003964 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003965
3966 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003967 command = args + files
3968 if options.filter:
3969 command = ['--filter=' + ','.join(options.filter)] + command
3970 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003971
3972 white_regex = re.compile(settings.GetLintRegex())
3973 black_regex = re.compile(settings.GetLintIgnoreRegex())
3974 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3975 for filename in filenames:
3976 if white_regex.match(filename):
3977 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003978 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003979 else:
3980 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3981 extra_check_functions)
3982 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003983 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003984 finally:
3985 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003987 if cpplint._cpplint_state.error_count != 0:
3988 return 1
3989 return 0
3990
3991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003993 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003994 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003996 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003997 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003998 auth.add_auth_options(parser)
3999 options, args = parser.parse_args(args)
4000 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001
sbc@chromium.org71437c02015-04-09 19:29:40 +00004002 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004003 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004 return 1
4005
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004006 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007 if args:
4008 base_branch = args[0]
4009 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004010 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004011 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004012
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004013 cl.RunHook(
4014 committing=not options.upload,
4015 may_prompt=False,
4016 verbose=options.verbose,
4017 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004018 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019
4020
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004021def GenerateGerritChangeId(message):
4022 """Returns Ixxxxxx...xxx change id.
4023
4024 Works the same way as
4025 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4026 but can be called on demand on all platforms.
4027
4028 The basic idea is to generate git hash of a state of the tree, original commit
4029 message, author/committer info and timestamps.
4030 """
4031 lines = []
4032 tree_hash = RunGitSilent(['write-tree'])
4033 lines.append('tree %s' % tree_hash.strip())
4034 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4035 if code == 0:
4036 lines.append('parent %s' % parent.strip())
4037 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4038 lines.append('author %s' % author.strip())
4039 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4040 lines.append('committer %s' % committer.strip())
4041 lines.append('')
4042 # Note: Gerrit's commit-hook actually cleans message of some lines and
4043 # whitespace. This code is not doing this, but it clearly won't decrease
4044 # entropy.
4045 lines.append(message)
4046 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4047 stdin='\n'.join(lines))
4048 return 'I%s' % change_hash.strip()
4049
4050
wittman@chromium.org455dc922015-01-26 20:15:50 +00004051def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4052 """Computes the remote branch ref to use for the CL.
4053
4054 Args:
4055 remote (str): The git remote for the CL.
4056 remote_branch (str): The git remote branch for the CL.
4057 target_branch (str): The target branch specified by the user.
4058 pending_prefix (str): The pending prefix from the settings.
4059 """
4060 if not (remote and remote_branch):
4061 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004062
wittman@chromium.org455dc922015-01-26 20:15:50 +00004063 if target_branch:
4064 # Cannonicalize branch references to the equivalent local full symbolic
4065 # refs, which are then translated into the remote full symbolic refs
4066 # below.
4067 if '/' not in target_branch:
4068 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4069 else:
4070 prefix_replacements = (
4071 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4072 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4073 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4074 )
4075 match = None
4076 for regex, replacement in prefix_replacements:
4077 match = re.search(regex, target_branch)
4078 if match:
4079 remote_branch = target_branch.replace(match.group(0), replacement)
4080 break
4081 if not match:
4082 # This is a branch path but not one we recognize; use as-is.
4083 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004084 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4085 # Handle the refs that need to land in different refs.
4086 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004087
wittman@chromium.org455dc922015-01-26 20:15:50 +00004088 # Create the true path to the remote branch.
4089 # Does the following translation:
4090 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4091 # * refs/remotes/origin/master -> refs/heads/master
4092 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4093 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4094 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4095 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4096 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4097 'refs/heads/')
4098 elif remote_branch.startswith('refs/remotes/branch-heads'):
4099 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4100 # If a pending prefix exists then replace refs/ with it.
4101 if pending_prefix:
4102 remote_branch = remote_branch.replace('refs/', pending_prefix)
4103 return remote_branch
4104
4105
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004106def cleanup_list(l):
4107 """Fixes a list so that comma separated items are put as individual items.
4108
4109 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4110 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4111 """
4112 items = sum((i.split(',') for i in l), [])
4113 stripped_items = (i.strip() for i in items)
4114 return sorted(filter(None, stripped_items))
4115
4116
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004117@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004118def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004119 """Uploads the current changelist to codereview.
4120
4121 Can skip dependency patchset uploads for a branch by running:
4122 git config branch.branch_name.skip-deps-uploads True
4123 To unset run:
4124 git config --unset branch.branch_name.skip-deps-uploads
4125 Can also set the above globally by using the --global flag.
4126 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004127 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4128 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004129 parser.add_option('--bypass-watchlists', action='store_true',
4130 dest='bypass_watchlists',
4131 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004132 parser.add_option('-f', action='store_true', dest='force',
4133 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004134 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004135 parser.add_option('-b', '--bug',
4136 help='pre-populate the bug number(s) for this issue. '
4137 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004138 parser.add_option('--message-file', dest='message_file',
4139 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004140 parser.add_option('-t', dest='title',
4141 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004142 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004143 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004144 help='reviewer email addresses')
4145 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004146 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004147 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004148 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004149 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004150 parser.add_option('--emulate_svn_auto_props',
4151 '--emulate-svn-auto-props',
4152 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004153 dest="emulate_svn_auto_props",
4154 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004155 parser.add_option('-c', '--use-commit-queue', action='store_true',
4156 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004157 parser.add_option('--private', action='store_true',
4158 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004159 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004160 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004161 metavar='TARGET',
4162 help='Apply CL to remote ref TARGET. ' +
4163 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004164 parser.add_option('--squash', action='store_true',
4165 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004166 parser.add_option('--no-squash', action='store_true',
4167 help='Don\'t squash multiple commits into one ' +
4168 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004169 parser.add_option('--topic', default=None,
4170 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004171 parser.add_option('--email', default=None,
4172 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004173 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4174 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004175 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4176 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004177 help='Send the patchset to do a CQ dry run right after '
4178 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004179 parser.add_option('--dependencies', action='store_true',
4180 help='Uploads CLs of all the local branches that depend on '
4181 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004182
rmistry@google.com2dd99862015-06-22 12:22:18 +00004183 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004184 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004185 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004186 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004187 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004188 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004189 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004190
sbc@chromium.org71437c02015-04-09 19:29:40 +00004191 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004192 return 1
4193
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004194 options.reviewers = cleanup_list(options.reviewers)
4195 options.cc = cleanup_list(options.cc)
4196
tandriib80458a2016-06-23 12:20:07 -07004197 if options.message_file:
4198 if options.message:
4199 parser.error('only one of --message and --message-file allowed.')
4200 options.message = gclient_utils.FileRead(options.message_file)
4201 options.message_file = None
4202
tandrii4d0545a2016-07-06 03:56:49 -07004203 if options.cq_dry_run and options.use_commit_queue:
4204 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4205
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004206 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4207 settings.GetIsGerrit()
4208
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004209 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004210 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004211
4212
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004213def IsSubmoduleMergeCommit(ref):
4214 # When submodules are added to the repo, we expect there to be a single
4215 # non-git-svn merge commit at remote HEAD with a signature comment.
4216 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004217 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004218 return RunGit(cmd) != ''
4219
4220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004222 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004224 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4225 upstream and closes the issue automatically and atomically.
4226
4227 Otherwise (in case of Rietveld):
4228 Squashes branch into a single commit.
Andrii Shyshkalov06a25022016-11-24 16:47:00 +01004229 Updates commit message with metadata (e.g. pointer to review).
4230 Pushes the code upstream.
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004231 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004232 """
4233 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4234 help='bypass upload presubmit hook')
4235 parser.add_option('-m', dest='message',
4236 help="override review description")
4237 parser.add_option('-f', action='store_true', dest='force',
4238 help="force yes to questions (don't prompt)")
4239 parser.add_option('-c', dest='contributor',
4240 help="external contributor for patch (appended to " +
4241 "description and used as author for git). Should be " +
4242 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004243 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004244 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004246 auth_config = auth.extract_auth_config_from_options(options)
4247
4248 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004250 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4251 if cl.IsGerrit():
4252 if options.message:
4253 # This could be implemented, but it requires sending a new patch to
4254 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4255 # Besides, Gerrit has the ability to change the commit message on submit
4256 # automatically, thus there is no need to support this option (so far?).
4257 parser.error('-m MESSAGE option is not supported for Gerrit.')
4258 if options.contributor:
4259 parser.error(
4260 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4261 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4262 'the contributor\'s "name <email>". If you can\'t upload such a '
4263 'commit for review, contact your repository admin and request'
4264 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004265 if not cl.GetIssue():
4266 DieWithError('You must upload the issue first to Gerrit.\n'
4267 ' If you would rather have `git cl land` upload '
4268 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004269 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4270 options.verbose)
4271
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004272 current = cl.GetBranch()
4273 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4274 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004275 print()
4276 print('Attempting to push branch %r into another local branch!' % current)
4277 print()
4278 print('Either reparent this branch on top of origin/master:')
4279 print(' git reparent-branch --root')
4280 print()
4281 print('OR run `git rebase-update` if you think the parent branch is ')
4282 print('already committed.')
4283 print()
4284 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004285 return 1
4286
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004287 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004288 # Default to merging against our best guess of the upstream branch.
4289 args = [cl.GetUpstreamBranch()]
4290
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004291 if options.contributor:
4292 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004293 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004294 return 1
4295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004297 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298
sbc@chromium.org71437c02015-04-09 19:29:40 +00004299 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300 return 1
4301
4302 # This rev-list syntax means "show all commits not in my branch that
4303 # are in base_branch".
4304 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4305 base_branch]).splitlines()
4306 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004307 print('Base branch "%s" has %d commits '
4308 'not in this branch.' % (base_branch, len(upstream_commits)))
4309 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004310 return 1
4311
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004312 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004313 svn_head = None
4314 if cmd == 'dcommit' or base_has_submodules:
4315 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4316 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004319 # If the base_head is a submodule merge commit, the first parent of the
4320 # base_head should be a git-svn commit, which is what we're interested in.
4321 base_svn_head = base_branch
4322 if base_has_submodules:
4323 base_svn_head += '^1'
4324
4325 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004326 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004327 print('This branch has %d additional commits not upstreamed yet.'
4328 % len(extra_commits.splitlines()))
4329 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4330 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331 return 1
4332
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004333 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004334 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004335 author = None
4336 if options.contributor:
4337 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004338 hook_results = cl.RunHook(
4339 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004340 may_prompt=not options.force,
4341 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004342 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004343 if not hook_results.should_continue():
4344 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004346 # Check the tree status if the tree status URL is set.
4347 status = GetTreeStatus()
4348 if 'closed' == status:
4349 print('The tree is closed. Please wait for it to reopen. Use '
4350 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4351 return 1
4352 elif 'unknown' == status:
4353 print('Unable to determine tree status. Please verify manually and '
4354 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4355 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004357 change_desc = ChangeDescription(options.message)
4358 if not change_desc.description and cl.GetIssue():
4359 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004361 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004362 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004363 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004364 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004365 print('No description set.')
4366 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004367 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004368
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004369 # Keep a separate copy for the commit message, because the commit message
4370 # contains the link to the Rietveld issue, while the Rietveld message contains
4371 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004372 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004373 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004374
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004375 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004376 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004377 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004378 # after it. Add a period on a new line to circumvent this. Also add a space
4379 # before the period to make sure that Gitiles continues to correctly resolve
4380 # the URL.
4381 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004383 commit_desc.append_footer('Patch from %s.' % options.contributor)
4384
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004385 print('Description:')
4386 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004388 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004390 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004392 # We want to squash all this branch's commits into one commit with the proper
4393 # description. We do this by doing a "reset --soft" to the base branch (which
4394 # keeps the working copy the same), then dcommitting that. If origin/master
4395 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4396 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004398 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4399 # Delete the branches if they exist.
4400 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4401 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4402 result = RunGitWithCode(showref_cmd)
4403 if result[0] == 0:
4404 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004405
4406 # We might be in a directory that's present in this branch but not in the
4407 # trunk. Move up to the top of the tree so that git commands that expect a
4408 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004409 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004410 if rel_base_path:
4411 os.chdir(rel_base_path)
4412
4413 # Stuff our change into the merge branch.
4414 # We wrap in a try...finally block so if anything goes wrong,
4415 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004416 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004417 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004418 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004419 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004420 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004421 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004422 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004424 RunGit(
4425 [
4426 'commit', '--author', options.contributor,
4427 '-m', commit_desc.description,
4428 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004429 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004430 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004431 if base_has_submodules:
4432 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4433 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4434 RunGit(['checkout', CHERRY_PICK_BRANCH])
4435 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004436 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004437 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004438 mirror = settings.GetGitMirror(remote)
4439 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004440 pending_prefix = settings.GetPendingRefPrefix()
4441 if not pending_prefix or branch.startswith(pending_prefix):
4442 # If not using refs/pending/heads/* at all, or target ref is already set
4443 # to pending, then push to the target ref directly.
4444 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004445 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004446 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004447 else:
4448 # Cherry-pick the change on top of pending ref and then push it.
4449 assert branch.startswith('refs/'), branch
4450 assert pending_prefix[-1] == '/', pending_prefix
4451 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004452 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004453 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004454 if retcode == 0:
4455 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456 else:
4457 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004458 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004459 'svn', 'dcommit',
4460 '-C%s' % options.similarity,
4461 '--no-rebase', '--rmdir',
4462 ]
4463 if settings.GetForceHttpsCommitUrl():
4464 # Allow forcing https commit URLs for some projects that don't allow
4465 # committing to http URLs (like Google Code).
4466 remote_url = cl.GetGitSvnRemoteUrl()
4467 if urlparse.urlparse(remote_url).scheme == 'http':
4468 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004469 cmd_args.append('--commit-url=%s' % remote_url)
4470 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004471 if 'Committed r' in output:
4472 revision = re.match(
4473 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4474 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475 finally:
4476 # And then swap back to the original branch and clean up.
4477 RunGit(['checkout', '-q', cl.GetBranch()])
4478 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004479 if base_has_submodules:
4480 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004481
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004482 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004483 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004484 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004485
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004486 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004487 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004488 try:
4489 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4490 # We set pushed_to_pending to False, since it made it all the way to the
4491 # real ref.
4492 pushed_to_pending = False
4493 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004494 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004497 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004498 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004499 if not to_pending:
4500 if viewvc_url and revision:
4501 change_desc.append_footer(
4502 'Committed: %s%s' % (viewvc_url, revision))
4503 elif revision:
4504 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004505 print('Closing issue '
4506 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004507 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004508 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004509 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004510 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004511 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004512 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004513 if options.bypass_hooks:
4514 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4515 else:
4516 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004517 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004518
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004519 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004520 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004521 print('The commit is in the pending queue (%s).' % pending_ref)
4522 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4523 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004524
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004525 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4526 if os.path.isfile(hook):
4527 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004528
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004529 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530
4531
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004532def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004533 print()
4534 print('Waiting for commit to be landed on %s...' % real_ref)
4535 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004536 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4537 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004538 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004539
4540 loop = 0
4541 while True:
4542 sys.stdout.write('fetching (%d)... \r' % loop)
4543 sys.stdout.flush()
4544 loop += 1
4545
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004546 if mirror:
4547 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004548 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4549 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4550 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4551 for commit in commits.splitlines():
4552 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004553 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004554 return commit
4555
4556 current_rev = to_rev
4557
4558
tandriibf429402016-09-14 07:09:12 -07004559def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004560 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4561
4562 Returns:
4563 (retcode of last operation, output log of last operation).
4564 """
4565 assert pending_ref.startswith('refs/'), pending_ref
4566 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4567 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4568 code = 0
4569 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004570 max_attempts = 3
4571 attempts_left = max_attempts
4572 while attempts_left:
4573 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004574 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004575 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004576
4577 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004578 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004579 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004580 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004581 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004582 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004583 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004584 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004585 continue
4586
4587 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004588 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004589 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004590 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004591 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004592 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4593 'the following files have merge conflicts:' % pending_ref)
4594 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4595 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004596 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004597 return code, out
4598
4599 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004600 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004601 code, out = RunGitWithCode(
4602 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4603 if code == 0:
4604 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004605 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004606 return code, out
4607
vapiera7fbd5a2016-06-16 09:17:49 -07004608 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004609 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004610 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004611 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004612 print('Fatal push error. Make sure your .netrc credentials and git '
4613 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004614 return code, out
4615
vapiera7fbd5a2016-06-16 09:17:49 -07004616 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004617 return code, out
4618
4619
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004620def IsFatalPushFailure(push_stdout):
4621 """True if retrying push won't help."""
4622 return '(prohibited by Gerrit)' in push_stdout
4623
4624
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004625@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004627 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004629 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004630 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004631 message = """This repository appears to be a git-svn mirror, but we
4632don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004633 else:
4634 message = """This doesn't appear to be an SVN repository.
4635If your project has a true, writeable git repository, you probably want to run
4636'git cl land' instead.
4637If your project has a git mirror of an upstream SVN master, you probably need
4638to run 'git svn init'.
4639
4640Using the wrong command might cause your commit to appear to succeed, and the
4641review to be closed, without actually landing upstream. If you choose to
4642proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004643 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004644 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004645 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4646 'Please let us know of this project you are committing to:'
4647 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004648 return SendUpstream(parser, args, 'dcommit')
4649
4650
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004651@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004652def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004653 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004654 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655 print('This appears to be an SVN repository.')
4656 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004657 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004658 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004659 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660
4661
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004662@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004663def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004664 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665 parser.add_option('-b', dest='newbranch',
4666 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004667 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004668 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004669 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4670 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004671 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004672 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004673 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004674 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004676 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004677
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004678
4679 group = optparse.OptionGroup(
4680 parser,
4681 'Options for continuing work on the current issue uploaded from a '
4682 'different clone (e.g. different machine). Must be used independently '
4683 'from the other options. No issue number should be specified, and the '
4684 'branch must have an issue number associated with it')
4685 group.add_option('--reapply', action='store_true', dest='reapply',
4686 help='Reset the branch and reapply the issue.\n'
4687 'CAUTION: This will undo any local changes in this '
4688 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004689
4690 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004691 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004692 parser.add_option_group(group)
4693
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004694 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004695 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004697 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004698 auth_config = auth.extract_auth_config_from_options(options)
4699
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004700
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004701 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004702 if options.newbranch:
4703 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004704 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004705 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004706
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004707 cl = Changelist(auth_config=auth_config,
4708 codereview=options.forced_codereview)
4709 if not cl.GetIssue():
4710 parser.error('current branch must have an associated issue')
4711
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004712 upstream = cl.GetUpstreamBranch()
4713 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004714 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004715
4716 RunGit(['reset', '--hard', upstream])
4717 if options.pull:
4718 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004719
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004720 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4721 options.directory)
4722
4723 if len(args) != 1 or not args[0]:
4724 parser.error('Must specify issue number or url')
4725
4726 # We don't want uncommitted changes mixed up with the patch.
4727 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004728 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004730 if options.newbranch:
4731 if options.force:
4732 RunGit(['branch', '-D', options.newbranch],
4733 stderr=subprocess2.PIPE, error_ok=True)
4734 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004735 elif not GetCurrentBranch():
4736 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004737
4738 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4739
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004740 if cl.IsGerrit():
4741 if options.reject:
4742 parser.error('--reject is not supported with Gerrit codereview.')
4743 if options.nocommit:
4744 parser.error('--nocommit is not supported with Gerrit codereview.')
4745 if options.directory:
4746 parser.error('--directory is not supported with Gerrit codereview.')
4747
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004748 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004749 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004750
4751
4752def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004753 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004754 # Provide a wrapper for git svn rebase to help avoid accidental
4755 # git svn dcommit.
4756 # It's the only command that doesn't use parser at all since we just defer
4757 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004758
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004759 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760
4761
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004762def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004763 """Fetches the tree status and returns either 'open', 'closed',
4764 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004765 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004766 if url:
4767 status = urllib2.urlopen(url).read().lower()
4768 if status.find('closed') != -1 or status == '0':
4769 return 'closed'
4770 elif status.find('open') != -1 or status == '1':
4771 return 'open'
4772 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004773 return 'unset'
4774
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776def GetTreeStatusReason():
4777 """Fetches the tree status from a json url and returns the message
4778 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004779 url = settings.GetTreeStatusUrl()
4780 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004781 connection = urllib2.urlopen(json_url)
4782 status = json.loads(connection.read())
4783 connection.close()
4784 return status['message']
4785
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004787def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004788 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004789 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004790 status = GetTreeStatus()
4791 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004792 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004793 return 2
4794
vapiera7fbd5a2016-06-16 09:17:49 -07004795 print('The tree is %s' % status)
4796 print()
4797 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004798 if status != 'open':
4799 return 1
4800 return 0
4801
4802
maruel@chromium.org15192402012-09-06 12:38:29 +00004803def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004804 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004805 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004806 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004807 '-b', '--bot', action='append',
4808 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4809 'times to specify multiple builders. ex: '
4810 '"-b win_rel -b win_layout". See '
4811 'the try server waterfall for the builders name and the tests '
4812 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004813 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004814 '-B', '--bucket', default='',
4815 help=('Buildbucket bucket to send the try requests.'))
4816 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004817 '-m', '--master', default='',
4818 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004819 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004820 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004821 help='Revision to use for the try job; default: the revision will '
4822 'be determined by the try recipe that builder runs, which usually '
4823 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004824 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004825 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004826 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004827 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004828 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004829 '--project',
4830 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004831 'in recipe to determine to which repository or directory to '
4832 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004833 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004834 '-p', '--property', dest='properties', action='append', default=[],
4835 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004836 'key2=value2 etc. The value will be treated as '
4837 'json if decodable, or as string otherwise. '
4838 'NOTE: using this may make your try job not usable for CQ, '
4839 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004840 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004841 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4842 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004843 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004844 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004845 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004846 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004847
machenbach@chromium.org45453142015-09-15 08:45:22 +00004848 # Make sure that all properties are prop=value pairs.
4849 bad_params = [x for x in options.properties if '=' not in x]
4850 if bad_params:
4851 parser.error('Got properties with missing "=": %s' % bad_params)
4852
maruel@chromium.org15192402012-09-06 12:38:29 +00004853 if args:
4854 parser.error('Unknown arguments: %s' % args)
4855
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004856 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004857 if not cl.GetIssue():
4858 parser.error('Need to upload first')
4859
tandriie113dfd2016-10-11 10:20:12 -07004860 error_message = cl.CannotTriggerTryJobReason()
4861 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004862 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004863
borenet6c0efe62016-10-19 08:13:29 -07004864 if options.bucket and options.master:
4865 parser.error('Only one of --bucket and --master may be used.')
4866
qyearsley1fdfcb62016-10-24 13:22:03 -07004867 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004868
qyearsleydd49f942016-10-28 11:57:22 -07004869 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4870 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004871 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004872 if options.verbose:
4873 print('git cl try with no bots now defaults to CQ Dry Run.')
4874 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004875
borenet6c0efe62016-10-19 08:13:29 -07004876 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004877 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004878 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004879 'of bot requires an initial job from a parent (usually a builder). '
4880 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004881 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004882 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004883
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004884 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004885 # TODO(tandrii): Checking local patchset against remote patchset is only
4886 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4887 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004888 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
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004939 # TODO(tandrii): Checking local patchset against remote patchset is only
4940 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4941 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004942 print('Warning: Codereview server has newer patchsets (%s) than most '
4943 'recent upload from local checkout (%s). Did a previous upload '
4944 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004945 'By default, git cl try-results uses the latest patchset from '
4946 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004947 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004948 try:
tandrii221ab252016-10-06 08:12:04 -07004949 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004950 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004951 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004952 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004953 if options.json:
4954 write_try_results_json(options.json, jobs)
4955 else:
4956 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004957 return 0
4958
4959
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004960@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004961def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004962 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004963 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004964 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004965 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004967 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004968 if args:
4969 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004970 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004971 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004972 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004973 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004974
4975 # Clear configured merge-base, if there is one.
4976 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004977 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004978 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004979 return 0
4980
4981
thestig@chromium.org00858c82013-12-02 23:08:03 +00004982def CMDweb(parser, args):
4983 """Opens the current CL in the web browser."""
4984 _, args = parser.parse_args(args)
4985 if args:
4986 parser.error('Unrecognized args: %s' % ' '.join(args))
4987
4988 issue_url = Changelist().GetIssueURL()
4989 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004990 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004991 return 1
4992
4993 webbrowser.open(issue_url)
4994 return 0
4995
4996
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004997def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004998 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004999 parser.add_option('-d', '--dry-run', action='store_true',
5000 help='trigger in dry run mode')
5001 parser.add_option('-c', '--clear', action='store_true',
5002 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005003 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005004 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005005 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005006 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005007 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005008 if args:
5009 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005010 if options.dry_run and options.clear:
5011 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5012
iannuccie53c9352016-08-17 14:40:40 -07005013 cl = Changelist(auth_config=auth_config, issue=options.issue,
5014 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005015 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005016 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005017 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005018 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005019 state = _CQState.DRY_RUN
5020 else:
5021 state = _CQState.COMMIT
5022 if not cl.GetIssue():
5023 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005024 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005025 return 0
5026
5027
groby@chromium.org411034a2013-02-26 15:12:01 +00005028def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005029 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005030 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005031 auth.add_auth_options(parser)
5032 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005033 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005034 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005035 if args:
5036 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005037 cl = Changelist(auth_config=auth_config, issue=options.issue,
5038 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005039 # Ensure there actually is an issue to close.
5040 cl.GetDescription()
5041 cl.CloseIssue()
5042 return 0
5043
5044
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005045def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005046 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005047 parser.add_option(
5048 '--stat',
5049 action='store_true',
5050 dest='stat',
5051 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005052 auth.add_auth_options(parser)
5053 options, args = parser.parse_args(args)
5054 auth_config = auth.extract_auth_config_from_options(options)
5055 if args:
5056 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005057
5058 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005059 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005060 # Staged changes would be committed along with the patch from last
5061 # upload, hence counted toward the "last upload" side in the final
5062 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005063 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005064 return 1
5065
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005066 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005067 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005068 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005069 if not issue:
5070 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005071 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005072 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005073
5074 # Create a new branch based on the merge-base
5075 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005076 # Clear cached branch in cl object, to avoid overwriting original CL branch
5077 # properties.
5078 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005079 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005080 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005081 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005082 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005083 return rtn
5084
wychen@chromium.org06928532015-02-03 02:11:29 +00005085 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005086 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005087 cmd = ['git', 'diff']
5088 if options.stat:
5089 cmd.append('--stat')
5090 cmd.extend([TMP_BRANCH, branch, '--'])
5091 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005092 finally:
5093 RunGit(['checkout', '-q', branch])
5094 RunGit(['branch', '-D', TMP_BRANCH])
5095
5096 return 0
5097
5098
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005099def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005100 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005101 parser.add_option(
5102 '--no-color',
5103 action='store_true',
5104 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005105 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005106 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005107 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005108
5109 author = RunGit(['config', 'user.email']).strip() or None
5110
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005111 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005112
5113 if args:
5114 if len(args) > 1:
5115 parser.error('Unknown args')
5116 base_branch = args[0]
5117 else:
5118 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005119 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005120
5121 change = cl.GetChange(base_branch, None)
5122 return owners_finder.OwnersFinder(
5123 [f.LocalPath() for f in
5124 cl.GetChange(base_branch, None).AffectedFiles()],
5125 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005126 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005127 disable_color=options.no_color).run()
5128
5129
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005130def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005131 """Generates a diff command."""
5132 # Generate diff for the current branch's changes.
5133 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5134 upstream_commit, '--' ]
5135
5136 if args:
5137 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005138 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005139 diff_cmd.append(arg)
5140 else:
5141 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005142
5143 return diff_cmd
5144
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005145def MatchingFileType(file_name, extensions):
5146 """Returns true if the file name ends with one of the given extensions."""
5147 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005148
enne@chromium.org555cfe42014-01-29 18:21:39 +00005149@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005150def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005151 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005152 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005153 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005154 parser.add_option('--full', action='store_true',
5155 help='Reformat the full content of all touched files')
5156 parser.add_option('--dry-run', action='store_true',
5157 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005158 parser.add_option('--python', action='store_true',
5159 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005160 parser.add_option('--diff', action='store_true',
5161 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005162 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005163
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005164 # git diff generates paths against the root of the repository. Change
5165 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005166 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005167 if rel_base_path:
5168 os.chdir(rel_base_path)
5169
digit@chromium.org29e47272013-05-17 17:01:46 +00005170 # Grab the merge-base commit, i.e. the upstream commit of the current
5171 # branch when it was created or the last time it was rebased. This is
5172 # to cover the case where the user may have called "git fetch origin",
5173 # moving the origin branch to a newer commit, but hasn't rebased yet.
5174 upstream_commit = None
5175 cl = Changelist()
5176 upstream_branch = cl.GetUpstreamBranch()
5177 if upstream_branch:
5178 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5179 upstream_commit = upstream_commit.strip()
5180
5181 if not upstream_commit:
5182 DieWithError('Could not find base commit for this branch. '
5183 'Are you in detached state?')
5184
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005185 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5186 diff_output = RunGit(changed_files_cmd)
5187 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005188 # Filter out files deleted by this CL
5189 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005190
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005191 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5192 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5193 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005194 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005195
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005196 top_dir = os.path.normpath(
5197 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5198
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005199 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5200 # formatted. This is used to block during the presubmit.
5201 return_value = 0
5202
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005203 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005204 # Locate the clang-format binary in the checkout
5205 try:
5206 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005207 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005208 DieWithError(e)
5209
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005210 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005211 cmd = [clang_format_tool]
5212 if not opts.dry_run and not opts.diff:
5213 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005214 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005215 if opts.diff:
5216 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005217 else:
5218 env = os.environ.copy()
5219 env['PATH'] = str(os.path.dirname(clang_format_tool))
5220 try:
5221 script = clang_format.FindClangFormatScriptInChromiumTree(
5222 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005223 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005224 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005225
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005226 cmd = [sys.executable, script, '-p0']
5227 if not opts.dry_run and not opts.diff:
5228 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005229
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005230 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5231 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005232
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005233 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5234 if opts.diff:
5235 sys.stdout.write(stdout)
5236 if opts.dry_run and len(stdout) > 0:
5237 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005238
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005239 # Similar code to above, but using yapf on .py files rather than clang-format
5240 # on C/C++ files
5241 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005242 yapf_tool = gclient_utils.FindExecutable('yapf')
5243 if yapf_tool is None:
5244 DieWithError('yapf not found in PATH')
5245
5246 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005247 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005248 cmd = [yapf_tool]
5249 if not opts.dry_run and not opts.diff:
5250 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005251 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005252 if opts.diff:
5253 sys.stdout.write(stdout)
5254 else:
5255 # TODO(sbc): yapf --lines mode still has some issues.
5256 # https://github.com/google/yapf/issues/154
5257 DieWithError('--python currently only works with --full')
5258
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005259 # Dart's formatter does not have the nice property of only operating on
5260 # modified chunks, so hard code full.
5261 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005262 try:
5263 command = [dart_format.FindDartFmtToolInChromiumTree()]
5264 if not opts.dry_run and not opts.diff:
5265 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005266 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005267
ppi@chromium.org6593d932016-03-03 15:41:15 +00005268 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005269 if opts.dry_run and stdout:
5270 return_value = 2
5271 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005272 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5273 'found in this checkout. Files in other languages are still '
5274 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005275
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005276 # Format GN build files. Always run on full build files for canonical form.
5277 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005278 cmd = ['gn', 'format' ]
5279 if opts.dry_run or opts.diff:
5280 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005281 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005282 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5283 shell=sys.platform == 'win32',
5284 cwd=top_dir)
5285 if opts.dry_run and gn_ret == 2:
5286 return_value = 2 # Not formatted.
5287 elif opts.diff and gn_ret == 2:
5288 # TODO this should compute and print the actual diff.
5289 print("This change has GN build file diff for " + gn_diff_file)
5290 elif gn_ret != 0:
5291 # For non-dry run cases (and non-2 return values for dry-run), a
5292 # nonzero error code indicates a failure, probably because the file
5293 # doesn't parse.
5294 DieWithError("gn format failed on " + gn_diff_file +
5295 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005296
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005297 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005298
5299
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005300@subcommand.usage('<codereview url or issue id>')
5301def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005302 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005303 _, args = parser.parse_args(args)
5304
5305 if len(args) != 1:
5306 parser.print_help()
5307 return 1
5308
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005309 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005310 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005311 parser.print_help()
5312 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005313 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005314
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005315 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005316 output = RunGit(['config', '--local', '--get-regexp',
5317 r'branch\..*\.%s' % issueprefix],
5318 error_ok=True)
5319 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005320 if issue == target_issue:
5321 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005322
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005323 branches = []
5324 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005325 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005326 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005327 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005328 return 1
5329 if len(branches) == 1:
5330 RunGit(['checkout', branches[0]])
5331 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005332 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005333 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005334 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005335 which = raw_input('Choose by index: ')
5336 try:
5337 RunGit(['checkout', branches[int(which)]])
5338 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005339 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005340 return 1
5341
5342 return 0
5343
5344
maruel@chromium.org29404b52014-09-08 22:58:00 +00005345def CMDlol(parser, args):
5346 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005347 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005348 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5349 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5350 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005351 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005352 return 0
5353
5354
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005355class OptionParser(optparse.OptionParser):
5356 """Creates the option parse and add --verbose support."""
5357 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005358 optparse.OptionParser.__init__(
5359 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005360 self.add_option(
5361 '-v', '--verbose', action='count', default=0,
5362 help='Use 2 times for more debugging info')
5363
5364 def parse_args(self, args=None, values=None):
5365 options, args = optparse.OptionParser.parse_args(self, args, values)
5366 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5367 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5368 return options, args
5369
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005371def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005372 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005373 print('\nYour python version %s is unsupported, please upgrade.\n' %
5374 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005375 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005376
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005377 # Reload settings.
5378 global settings
5379 settings = Settings()
5380
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005381 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005382 dispatcher = subcommand.CommandDispatcher(__name__)
5383 try:
5384 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005385 except auth.AuthenticationError as e:
5386 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005387 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005388 if e.code != 500:
5389 raise
5390 DieWithError(
5391 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5392 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005393 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005394
5395
5396if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005397 # These affect sys.stdout so do it outside of main() to simplify mocks in
5398 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005399 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005400 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005401 try:
5402 sys.exit(main(sys.argv[1:]))
5403 except KeyboardInterrupt:
5404 sys.stderr.write('interrupted\n')
5405 sys.exit(1)