blob: 1321d2f49d835cab504d282f4c9d557ce3dd9d38 [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)
352
qyearsley1fdfcb62016-10-24 13:22:03 -0700353 masters = presubmit_support.DoGetTryMasters(
354 change=change,
355 changed_files=change.LocalPaths(),
356 repository_root=settings.GetRoot(),
357 default_presubmit=None,
358 project=None,
359 verbose=options.verbose,
360 output_stream=sys.stdout)
361
362 if masters:
363 return masters
364
365 # Fall back to deprecated method: get try slaves from PRESUBMIT.py
366 # files.
qyearsley123a4682016-10-26 09:12:17 -0700367 # TODO(qyearsley): Remove this.
qyearsley1fdfcb62016-10-24 13:22:03 -0700368 options.bot = presubmit_support.DoGetTrySlaves(
369 change=change,
370 changed_files=change.LocalPaths(),
371 repository_root=settings.GetRoot(),
372 default_presubmit=None,
373 project=None,
374 verbose=options.verbose,
375 output_stream=sys.stdout)
376
377 if not options.bot:
378 return {}
379
qyearsleydd49f942016-10-28 11:57:22 -0700380 # If a bucket or master is passed, then we assume all bots are under
381 # that one master.
qyearsley1fdfcb62016-10-24 13:22:03 -0700382 if options.bucket:
383 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700384 if options.master:
385 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700386
qyearsleydd49f942016-10-28 11:57:22 -0700387 # If bots are listed but no master or bucket, then we need to find out
388 # the corresponding master for each bot.
389 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
390 if error_message:
391 option_parser.error(
392 'Tryserver master cannot be found because: %s\n'
393 'Please manually specify the tryserver master, e.g. '
394 '"-m tryserver.chromium.linux".' % error_message)
395 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
397
qyearsley123a4682016-10-26 09:12:17 -0700398def _get_bucket_map_for_builders(builders):
399 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 map_url = 'https://builders-map.appspot.com/'
401 try:
qyearsley123a4682016-10-26 09:12:17 -0700402 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700403 except urllib2.URLError as e:
404 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
405 (map_url, e))
406 except ValueError as e:
407 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700408 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 return None, 'Failed to build master map.'
410
qyearsley123a4682016-10-26 09:12:17 -0700411 bucket_map = {}
412 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700413 masters = builders_map.get(builder, [])
414 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700415 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700416 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700417 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700418 (builder, masters))
419 bucket = _prefix_master(masters[0])
420 bucket_map.setdefault(bucket, {})[builder] = []
421
422 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700423
424
borenet6c0efe62016-10-19 08:13:29 -0700425def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700426 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 """Sends a request to Buildbucket to trigger try jobs for a changelist.
428
429 Args:
430 auth_config: AuthConfig for Rietveld.
431 changelist: Changelist that the try jobs are associated with.
432 buckets: A nested dict mapping bucket names to builders to tests.
433 options: Command-line options.
434 """
tandriide281ae2016-10-12 06:02:30 -0700435 assert changelist.GetIssue(), 'CL must be uploaded first'
436 codereview_url = changelist.GetCodereviewServer()
437 assert codereview_url, 'CL must be uploaded first'
438 patchset = patchset or changelist.GetMostRecentPatchset()
439 assert patchset, 'CL must be uploaded first'
440
441 codereview_host = urlparse.urlparse(codereview_url).hostname
442 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000443 http = authenticator.authorize(httplib2.Http())
444 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700445
446 # TODO(tandrii): consider caching Gerrit CL details just like
447 # _RietveldChangelistImpl does, then caching values in these two variables
448 # won't be necessary.
449 owner_email = changelist.GetIssueOwner()
450 project = changelist.GetIssueProject()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000451
452 buildbucket_put_url = (
453 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000454 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700455 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
456 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
457 hostname=codereview_host,
458 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 patch=patchset)
tandriide281ae2016-10-12 06:02:30 -0700460 extra_properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000461
462 batch_req_body = {'builds': []}
463 print_text = []
464 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700465 for bucket, builders_and_tests in sorted(buckets.iteritems()):
466 print_text.append('Bucket: %s' % bucket)
467 master = None
468 if bucket.startswith(MASTER_PREFIX):
469 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000470 for builder, tests in sorted(builders_and_tests.iteritems()):
471 print_text.append(' %s: %s' % (builder, tests))
472 parameters = {
473 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000474 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700475 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000476 'revision': options.revision,
477 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000478 'properties': {
479 'category': category,
tandriide281ae2016-10-12 06:02:30 -0700480 'issue': changelist.GetIssue(),
tandriide281ae2016-10-12 06:02:30 -0700481 'patch_project': project,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000482 'patch_storage': 'rietveld',
483 'patchset': patchset,
tandriide281ae2016-10-12 06:02:30 -0700484 'rietveld': codereview_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 },
486 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000487 if 'presubmit' in builder.lower():
488 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000489 if tests:
490 parameters['properties']['testfilter'] = tests
tandriide281ae2016-10-12 06:02:30 -0700491 if extra_properties:
492 parameters['properties'].update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000493 if options.clobber:
494 parameters['properties']['clobber'] = True
borenet6c0efe62016-10-19 08:13:29 -0700495
496 tags = [
497 'builder:%s' % builder,
498 'buildset:%s' % buildset,
499 'user_agent:git_cl_try',
500 ]
501 if master:
502 parameters['properties']['master'] = master
503 tags.append('master:%s' % master)
504
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000505 batch_req_body['builds'].append(
506 {
507 'bucket': bucket,
508 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700510 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000511 }
512 )
513
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700515 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 http,
517 buildbucket_put_url,
518 'PUT',
519 body=json.dumps(batch_req_body),
520 headers={'Content-Type': 'application/json'}
521 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000522 print_text.append('To see results here, run: git cl try-results')
523 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700524 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000525
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000526
tandrii221ab252016-10-06 08:12:04 -0700527def fetch_try_jobs(auth_config, changelist, buildbucket_host,
528 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700529 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000530
qyearsley53f48a12016-09-01 10:45:13 -0700531 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532 """
tandrii221ab252016-10-06 08:12:04 -0700533 assert buildbucket_host
534 assert changelist.GetIssue(), 'CL must be uploaded first'
535 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
536 patchset = patchset or changelist.GetMostRecentPatchset()
537 assert patchset, 'CL must be uploaded first'
538
539 codereview_url = changelist.GetCodereviewServer()
540 codereview_host = urlparse.urlparse(codereview_url).hostname
541 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 if authenticator.has_cached_credentials():
543 http = authenticator.authorize(httplib2.Http())
544 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700545 print('Warning: Some results might be missing because %s' %
546 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700547 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 http = httplib2.Http()
549
550 http.force_exception_to_status_code = True
551
tandrii221ab252016-10-06 08:12:04 -0700552 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
553 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
554 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700556 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 params = {'tag': 'buildset:%s' % buildset}
558
559 builds = {}
560 while True:
561 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700562 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700564 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 for build in content.get('builds', []):
566 builds[build['id']] = build
567 if 'next_cursor' in content:
568 params['start_cursor'] = content['next_cursor']
569 else:
570 break
571 return builds
572
573
qyearsleyeab3c042016-08-24 09:18:28 -0700574def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 """Prints nicely result of fetch_try_jobs."""
576 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700577 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 return
579
580 # Make a copy, because we'll be modifying builds dictionary.
581 builds = builds.copy()
582 builder_names_cache = {}
583
584 def get_builder(b):
585 try:
586 return builder_names_cache[b['id']]
587 except KeyError:
588 try:
589 parameters = json.loads(b['parameters_json'])
590 name = parameters['builder_name']
591 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700592 print('WARNING: failed to get builder name for build %s: %s' % (
593 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 name = None
595 builder_names_cache[b['id']] = name
596 return name
597
598 def get_bucket(b):
599 bucket = b['bucket']
600 if bucket.startswith('master.'):
601 return bucket[len('master.'):]
602 return bucket
603
604 if options.print_master:
605 name_fmt = '%%-%ds %%-%ds' % (
606 max(len(str(get_bucket(b))) for b in builds.itervalues()),
607 max(len(str(get_builder(b))) for b in builds.itervalues()))
608 def get_name(b):
609 return name_fmt % (get_bucket(b), get_builder(b))
610 else:
611 name_fmt = '%%-%ds' % (
612 max(len(str(get_builder(b))) for b in builds.itervalues()))
613 def get_name(b):
614 return name_fmt % get_builder(b)
615
616 def sort_key(b):
617 return b['status'], b.get('result'), get_name(b), b.get('url')
618
619 def pop(title, f, color=None, **kwargs):
620 """Pop matching builds from `builds` dict and print them."""
621
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000622 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000623 colorize = str
624 else:
625 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
626
627 result = []
628 for b in builds.values():
629 if all(b.get(k) == v for k, v in kwargs.iteritems()):
630 builds.pop(b['id'])
631 result.append(b)
632 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700633 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000634 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700635 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000636
637 total = len(builds)
638 pop(status='COMPLETED', result='SUCCESS',
639 title='Successes:', color=Fore.GREEN,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
642 title='Infra Failures:', color=Fore.MAGENTA,
643 f=lambda b: (get_name(b), b.get('url')))
644 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
645 title='Failures:', color=Fore.RED,
646 f=lambda b: (get_name(b), b.get('url')))
647 pop(status='COMPLETED', result='CANCELED',
648 title='Canceled:', color=Fore.MAGENTA,
649 f=lambda b: (get_name(b),))
650 pop(status='COMPLETED', result='FAILURE',
651 failure_reason='INVALID_BUILD_DEFINITION',
652 title='Wrong master/builder name:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b),))
654 pop(status='COMPLETED', result='FAILURE',
655 title='Other failures:',
656 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
657 pop(status='COMPLETED',
658 title='Other finished:',
659 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
660 pop(status='STARTED',
661 title='Started:', color=Fore.YELLOW,
662 f=lambda b: (get_name(b), b.get('url')))
663 pop(status='SCHEDULED',
664 title='Scheduled:',
665 f=lambda b: (get_name(b), 'id=%s' % b['id']))
666 # The last section is just in case buildbucket API changes OR there is a bug.
667 pop(title='Other:',
668 f=lambda b: (get_name(b), 'id=%s' % b['id']))
669 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700670 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671
672
qyearsley53f48a12016-09-01 10:45:13 -0700673def write_try_results_json(output_file, builds):
674 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
675
676 The input |builds| dict is assumed to be generated by Buildbucket.
677 Buildbucket documentation: http://goo.gl/G0s101
678 """
679
680 def convert_build_dict(build):
681 return {
682 'buildbucket_id': build.get('id'),
683 'status': build.get('status'),
684 'result': build.get('result'),
685 'bucket': build.get('bucket'),
686 'builder_name': json.loads(
687 build.get('parameters_json', '{}')).get('builder_name'),
688 'failure_reason': build.get('failure_reason'),
689 'url': build.get('url'),
690 }
691
692 converted = []
693 for _, build in sorted(builds.items()):
694 converted.append(convert_build_dict(build))
695 write_json(output_file, converted)
696
697
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000698def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
699 """Return the corresponding git ref if |base_url| together with |glob_spec|
700 matches the full |url|.
701
702 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
703 """
704 fetch_suburl, as_ref = glob_spec.split(':')
705 if allow_wildcards:
706 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
707 if glob_match:
708 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
709 # "branches/{472,597,648}/src:refs/remotes/svn/*".
710 branch_re = re.escape(base_url)
711 if glob_match.group(1):
712 branch_re += '/' + re.escape(glob_match.group(1))
713 wildcard = glob_match.group(2)
714 if wildcard == '*':
715 branch_re += '([^/]*)'
716 else:
717 # Escape and replace surrounding braces with parentheses and commas
718 # with pipe symbols.
719 wildcard = re.escape(wildcard)
720 wildcard = re.sub('^\\\\{', '(', wildcard)
721 wildcard = re.sub('\\\\,', '|', wildcard)
722 wildcard = re.sub('\\\\}$', ')', wildcard)
723 branch_re += wildcard
724 if glob_match.group(3):
725 branch_re += re.escape(glob_match.group(3))
726 match = re.match(branch_re, url)
727 if match:
728 return re.sub('\*$', match.group(1), as_ref)
729
730 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
731 if fetch_suburl:
732 full_url = base_url + '/' + fetch_suburl
733 else:
734 full_url = base_url
735 if full_url == url:
736 return as_ref
737 return None
738
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000739
iannucci@chromium.org79540052012-10-19 23:15:26 +0000740def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000741 """Prints statistics about the change to the user."""
742 # --no-ext-diff is broken in some versions of Git, so try to work around
743 # this by overriding the environment (but there is still a problem if the
744 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000746 if 'GIT_EXTERNAL_DIFF' in env:
747 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000748
749 if find_copies:
750 similarity_options = ['--find-copies-harder', '-l100000',
751 '-C%s' % similarity]
752 else:
753 similarity_options = ['-M%s' % similarity]
754
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000755 try:
756 stdout = sys.stdout.fileno()
757 except AttributeError:
758 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000759 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000760 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000761 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000762 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000763
764
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000765class BuildbucketResponseException(Exception):
766 pass
767
768
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769class Settings(object):
770 def __init__(self):
771 self.default_server = None
772 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000773 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774 self.is_git_svn = None
775 self.svn_branch = None
776 self.tree_status_url = None
777 self.viewvc_url = None
778 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000779 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000780 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000781 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000782 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000783 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000784 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000785 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700786 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
788 def LazyUpdateIfNeeded(self):
789 """Updates the settings from a codereview.settings file, if available."""
790 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000791 # The only value that actually changes the behavior is
792 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000793 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000794 error_ok=True
795 ).strip().lower()
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000798 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799 LoadCodereviewSettingsFromFile(cr_settings_file)
800 self.updated = True
801
802 def GetDefaultServerUrl(self, error_ok=False):
803 if not self.default_server:
804 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000805 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 if error_ok:
808 return self.default_server
809 if not self.default_server:
810 error_message = ('Could not find settings file. You must configure '
811 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000812 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000813 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 return self.default_server
815
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000816 @staticmethod
817 def GetRelativeRoot():
818 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000819
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000821 if self.root is None:
822 self.root = os.path.abspath(self.GetRelativeRoot())
823 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000825 def GetGitMirror(self, remote='origin'):
826 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000827 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000828 if not os.path.isdir(local_url):
829 return None
830 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
831 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
832 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
833 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
834 if mirror.exists():
835 return mirror
836 return None
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 def GetIsGitSvn(self):
839 """Return true if this repo looks like it's using git-svn."""
840 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000841 if self.GetPendingRefPrefix():
842 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
843 self.is_git_svn = False
844 else:
845 # If you have any "svn-remote.*" config keys, we think you're using svn.
846 self.is_git_svn = RunGitWithCode(
847 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 return self.is_git_svn
849
850 def GetSVNBranch(self):
851 if self.svn_branch is None:
852 if not self.GetIsGitSvn():
853 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
854
855 # Try to figure out which remote branch we're based on.
856 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000857 # 1) iterate through our branch history and find the svn URL.
858 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
860 # regexp matching the git-svn line that contains the URL.
861 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
862
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000863 # We don't want to go through all of history, so read a line from the
864 # pipe at a time.
865 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000866 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000867 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
868 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000869 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000870 for line in proc.stdout:
871 match = git_svn_re.match(line)
872 if match:
873 url = match.group(1)
874 proc.stdout.close() # Cut pipe.
875 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000877 if url:
878 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
879 remotes = RunGit(['config', '--get-regexp',
880 r'^svn-remote\..*\.url']).splitlines()
881 for remote in remotes:
882 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000884 remote = match.group(1)
885 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000886 rewrite_root = RunGit(
887 ['config', 'svn-remote.%s.rewriteRoot' % remote],
888 error_ok=True).strip()
889 if rewrite_root:
890 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000891 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000892 ['config', 'svn-remote.%s.fetch' % remote],
893 error_ok=True).strip()
894 if fetch_spec:
895 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
896 if self.svn_branch:
897 break
898 branch_spec = RunGit(
899 ['config', 'svn-remote.%s.branches' % remote],
900 error_ok=True).strip()
901 if branch_spec:
902 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
903 if self.svn_branch:
904 break
905 tag_spec = RunGit(
906 ['config', 'svn-remote.%s.tags' % remote],
907 error_ok=True).strip()
908 if tag_spec:
909 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
910 if self.svn_branch:
911 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912
913 if not self.svn_branch:
914 DieWithError('Can\'t guess svn branch -- try specifying it on the '
915 'command line')
916
917 return self.svn_branch
918
919 def GetTreeStatusUrl(self, error_ok=False):
920 if not self.tree_status_url:
921 error_message = ('You must configure your tree status URL by running '
922 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000923 self.tree_status_url = self._GetRietveldConfig(
924 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 return self.tree_status_url
926
927 def GetViewVCUrl(self):
928 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000929 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 return self.viewvc_url
931
rmistry@google.com90752582014-01-14 21:04:50 +0000932 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000933 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000934
rmistry@google.com78948ed2015-07-08 23:09:57 +0000935 def GetIsSkipDependencyUpload(self, branch_name):
936 """Returns true if specified branch should skip dep uploads."""
937 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
938 error_ok=True)
939
rmistry@google.com5626a922015-02-26 14:03:30 +0000940 def GetRunPostUploadHook(self):
941 run_post_upload_hook = self._GetRietveldConfig(
942 'run-post-upload-hook', error_ok=True)
943 return run_post_upload_hook == "True"
944
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000945 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000946 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000947
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000948 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000949 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000950
ukai@chromium.orge8077812012-02-03 03:41:46 +0000951 def GetIsGerrit(self):
952 """Return true if this repo is assosiated with gerrit code review system."""
953 if self.is_gerrit is None:
954 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
955 return self.is_gerrit
956
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000957 def GetSquashGerritUploads(self):
958 """Return true if uploads to Gerrit should be squashed by default."""
959 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700960 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
961 if self.squash_gerrit_uploads is None:
962 # Default is squash now (http://crbug.com/611892#c23).
963 self.squash_gerrit_uploads = not (
964 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
965 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000966 return self.squash_gerrit_uploads
967
tandriia60502f2016-06-20 02:01:53 -0700968 def GetSquashGerritUploadsOverride(self):
969 """Return True or False if codereview.settings should be overridden.
970
971 Returns None if no override has been defined.
972 """
973 # See also http://crbug.com/611892#c23
974 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
975 error_ok=True).strip()
976 if result == 'true':
977 return True
978 if result == 'false':
979 return False
980 return None
981
tandrii@chromium.org28253532016-04-14 13:46:56 +0000982 def GetGerritSkipEnsureAuthenticated(self):
983 """Return True if EnsureAuthenticated should not be done for Gerrit
984 uploads."""
985 if self.gerrit_skip_ensure_authenticated is None:
986 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000987 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000988 error_ok=True).strip() == 'true')
989 return self.gerrit_skip_ensure_authenticated
990
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000991 def GetGitEditor(self):
992 """Return the editor specified in the git config, or None if none is."""
993 if self.git_editor is None:
994 self.git_editor = self._GetConfig('core.editor', error_ok=True)
995 return self.git_editor or None
996
thestig@chromium.org44202a22014-03-11 19:22:18 +0000997 def GetLintRegex(self):
998 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
999 DEFAULT_LINT_REGEX)
1000
1001 def GetLintIgnoreRegex(self):
1002 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
1003 DEFAULT_LINT_IGNORE_REGEX)
1004
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001005 def GetProject(self):
1006 if not self.project:
1007 self.project = self._GetRietveldConfig('project', error_ok=True)
1008 return self.project
1009
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001010 def GetForceHttpsCommitUrl(self):
1011 if not self.force_https_commit_url:
1012 self.force_https_commit_url = self._GetRietveldConfig(
1013 'force-https-commit-url', error_ok=True)
1014 return self.force_https_commit_url
1015
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001016 def GetPendingRefPrefix(self):
1017 if not self.pending_ref_prefix:
1018 self.pending_ref_prefix = self._GetRietveldConfig(
1019 'pending-ref-prefix', error_ok=True)
1020 return self.pending_ref_prefix
1021
tandriif46c20f2016-09-14 06:17:05 -07001022 def GetHasGitNumberFooter(self):
1023 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1024 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1025 if not self.git_number_footer:
1026 self.git_number_footer = self._GetRietveldConfig(
1027 'git-number-footer', error_ok=True)
1028 return self.git_number_footer
1029
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001030 def _GetRietveldConfig(self, param, **kwargs):
1031 return self._GetConfig('rietveld.' + param, **kwargs)
1032
rmistry@google.com78948ed2015-07-08 23:09:57 +00001033 def _GetBranchConfig(self, branch_name, param, **kwargs):
1034 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 def _GetConfig(self, param, **kwargs):
1037 self.LazyUpdateIfNeeded()
1038 return RunGit(['config', param], **kwargs).strip()
1039
1040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041def ShortBranchName(branch):
1042 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 return branch.replace('refs/heads/', '', 1)
1044
1045
1046def GetCurrentBranchRef():
1047 """Returns branch ref (e.g., refs/heads/master) or None."""
1048 return RunGit(['symbolic-ref', 'HEAD'],
1049 stderr=subprocess2.VOID, error_ok=True).strip() or None
1050
1051
1052def GetCurrentBranch():
1053 """Returns current branch or None.
1054
1055 For refs/heads/* branches, returns just last part. For others, full ref.
1056 """
1057 branchref = GetCurrentBranchRef()
1058 if branchref:
1059 return ShortBranchName(branchref)
1060 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
1062
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001063class _CQState(object):
1064 """Enum for states of CL with respect to Commit Queue."""
1065 NONE = 'none'
1066 DRY_RUN = 'dry_run'
1067 COMMIT = 'commit'
1068
1069 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1070
1071
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001072class _ParsedIssueNumberArgument(object):
1073 def __init__(self, issue=None, patchset=None, hostname=None):
1074 self.issue = issue
1075 self.patchset = patchset
1076 self.hostname = hostname
1077
1078 @property
1079 def valid(self):
1080 return self.issue is not None
1081
1082
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001083def ParseIssueNumberArgument(arg):
1084 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1085 fail_result = _ParsedIssueNumberArgument()
1086
1087 if arg.isdigit():
1088 return _ParsedIssueNumberArgument(issue=int(arg))
1089 if not arg.startswith('http'):
1090 return fail_result
1091 url = gclient_utils.UpgradeToHttps(arg)
1092 try:
1093 parsed_url = urlparse.urlparse(url)
1094 except ValueError:
1095 return fail_result
1096 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1097 tmp = cls.ParseIssueURL(parsed_url)
1098 if tmp is not None:
1099 return tmp
1100 return fail_result
1101
1102
tandriic2405f52016-10-10 08:13:15 -07001103class GerritIssueNotExists(Exception):
1104 def __init__(self, issue, url):
1105 self.issue = issue
1106 self.url = url
1107 super(GerritIssueNotExists, self).__init__()
1108
1109 def __str__(self):
1110 return 'issue %s at %s does not exist or you have no access to it' % (
1111 self.issue, self.url)
1112
1113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 """Changelist works with one changelist in local branch.
1116
1117 Supports two codereview backends: Rietveld or Gerrit, selected at object
1118 creation.
1119
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001120 Notes:
1121 * Not safe for concurrent multi-{thread,process} use.
1122 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001123 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001124 """
1125
1126 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1127 """Create a new ChangeList instance.
1128
1129 If issue is given, the codereview must be given too.
1130
1131 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1132 Otherwise, it's decided based on current configuration of the local branch,
1133 with default being 'rietveld' for backwards compatibility.
1134 See _load_codereview_impl for more details.
1135
1136 **kwargs will be passed directly to codereview implementation.
1137 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001139 global settings
1140 if not settings:
1141 # Happens when git_cl.py is used as a utility library.
1142 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143
1144 if issue:
1145 assert codereview, 'codereview must be known, if issue is known'
1146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.branchref = branchref
1148 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001149 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.branch = ShortBranchName(self.branchref)
1151 else:
1152 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001154 self.lookedup_issue = False
1155 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 self.has_description = False
1157 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001158 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001160 self.cc = None
1161 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001162 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001163
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001165 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001167 assert self._codereview_impl
1168 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169
1170 def _load_codereview_impl(self, codereview=None, **kwargs):
1171 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001172 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1173 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 return
1177
1178 # Automatic selection based on issue number set for a current branch.
1179 # Rietveld takes precedence over Gerrit.
1180 assert not self.issue
1181 # Whether we find issue or not, we are doing the lookup.
1182 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001183 if self.GetBranch():
1184 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1185 issue = _git_get_branch_config_value(
1186 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1187 if issue:
1188 self._codereview = codereview
1189 self._codereview_impl = cls(self, **kwargs)
1190 self.issue = int(issue)
1191 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001192
1193 # No issue is set for this branch, so decide based on repo-wide settings.
1194 return self._load_codereview_impl(
1195 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1196 **kwargs)
1197
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001198 def IsGerrit(self):
1199 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200
1201 def GetCCList(self):
1202 """Return the users cc'd on this CL.
1203
agable92bec4f2016-08-24 09:27:27 -07001204 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001205 """
1206 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001207 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001208 more_cc = ','.join(self.watchers)
1209 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1210 return self.cc
1211
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001212 def GetCCListWithoutDefault(self):
1213 """Return the users cc'd on this CL excluding default ones."""
1214 if self.cc is None:
1215 self.cc = ','.join(self.watchers)
1216 return self.cc
1217
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001218 def SetWatchers(self, watchers):
1219 """Set the list of email addresses that should be cc'd based on the changed
1220 files in this CL.
1221 """
1222 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
1224 def GetBranch(self):
1225 """Returns the short branch name, e.g. 'master'."""
1226 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001227 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001228 if not branchref:
1229 return None
1230 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 self.branch = ShortBranchName(self.branchref)
1232 return self.branch
1233
1234 def GetBranchRef(self):
1235 """Returns the full branch name, e.g. 'refs/heads/master'."""
1236 self.GetBranch() # Poke the lazy loader.
1237 return self.branchref
1238
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001239 def ClearBranch(self):
1240 """Clears cached branch data of this object."""
1241 self.branch = self.branchref = None
1242
tandrii5d48c322016-08-18 16:19:37 -07001243 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1244 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1245 kwargs['branch'] = self.GetBranch()
1246 return _git_get_branch_config_value(key, default, **kwargs)
1247
1248 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1249 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1250 assert self.GetBranch(), (
1251 'this CL must have an associated branch to %sset %s%s' %
1252 ('un' if value is None else '',
1253 key,
1254 '' if value is None else ' to %r' % value))
1255 kwargs['branch'] = self.GetBranch()
1256 return _git_set_branch_config_value(key, value, **kwargs)
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 @staticmethod
1259 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001260 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 e.g. 'origin', 'refs/heads/master'
1262 """
1263 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001264 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001267 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001269 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1270 error_ok=True).strip()
1271 if upstream_branch:
1272 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001274 # Fall back on trying a git-svn upstream branch.
1275 if settings.GetIsGitSvn():
1276 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001278 # Else, try to guess the origin remote.
1279 remote_branches = RunGit(['branch', '-r']).split()
1280 if 'origin/master' in remote_branches:
1281 # Fall back on origin/master if it exits.
1282 remote = 'origin'
1283 upstream_branch = 'refs/heads/master'
1284 elif 'origin/trunk' in remote_branches:
1285 # Fall back on origin/trunk if it exists. Generally a shared
1286 # git-svn clone
1287 remote = 'origin'
1288 upstream_branch = 'refs/heads/trunk'
1289 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001290 DieWithError(
1291 'Unable to determine default branch to diff against.\n'
1292 'Either pass complete "git diff"-style arguments, like\n'
1293 ' git cl upload origin/master\n'
1294 'or verify this branch is set up to track another \n'
1295 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296
1297 return remote, upstream_branch
1298
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001299 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001300 upstream_branch = self.GetUpstreamBranch()
1301 if not BranchExists(upstream_branch):
1302 DieWithError('The upstream for the current branch (%s) does not exist '
1303 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001304 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001305 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001306
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 def GetUpstreamBranch(self):
1308 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001311 upstream_branch = upstream_branch.replace('refs/heads/',
1312 'refs/remotes/%s/' % remote)
1313 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1314 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315 self.upstream_branch = upstream_branch
1316 return self.upstream_branch
1317
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001319 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 remote, branch = None, self.GetBranch()
1321 seen_branches = set()
1322 while branch not in seen_branches:
1323 seen_branches.add(branch)
1324 remote, branch = self.FetchUpstreamTuple(branch)
1325 branch = ShortBranchName(branch)
1326 if remote != '.' or branch.startswith('refs/remotes'):
1327 break
1328 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329 remotes = RunGit(['remote'], error_ok=True).split()
1330 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001332 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001333 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001334 logging.warning('Could not determine which remote this change is '
1335 'associated with, so defaulting to "%s". This may '
1336 'not be what you want. You may prevent this message '
1337 'by running "git svn info" as documented here: %s',
1338 self._remote,
1339 GIT_INSTRUCTIONS_URL)
1340 else:
1341 logging.warn('Could not determine which remote this change is '
1342 'associated with. You may prevent this message by '
1343 'running "git svn info" as documented here: %s',
1344 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 branch = 'HEAD'
1346 if branch.startswith('refs/remotes'):
1347 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001348 elif branch.startswith('refs/branch-heads/'):
1349 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001350 else:
1351 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001352 return self._remote
1353
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001354 def GitSanityChecks(self, upstream_git_obj):
1355 """Checks git repo status and ensures diff is from local commits."""
1356
sbc@chromium.org79706062015-01-14 21:18:12 +00001357 if upstream_git_obj is None:
1358 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001359 print('ERROR: unable to determine current branch (detached HEAD?)',
1360 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001361 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001362 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001363 return False
1364
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001365 # Verify the commit we're diffing against is in our current branch.
1366 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1367 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1368 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001369 print('ERROR: %s is not in the current branch. You may need to rebase '
1370 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001371 return False
1372
1373 # List the commits inside the diff, and verify they are all local.
1374 commits_in_diff = RunGit(
1375 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1376 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1377 remote_branch = remote_branch.strip()
1378 if code != 0:
1379 _, remote_branch = self.GetRemoteBranch()
1380
1381 commits_in_remote = RunGit(
1382 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1383
1384 common_commits = set(commits_in_diff) & set(commits_in_remote)
1385 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001386 print('ERROR: Your diff contains %d commits already in %s.\n'
1387 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1388 'the diff. If you are using a custom git flow, you can override'
1389 ' the reference used for this check with "git config '
1390 'gitcl.remotebranch <git-ref>".' % (
1391 len(common_commits), remote_branch, upstream_git_obj),
1392 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001393 return False
1394 return True
1395
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001396 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001397 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001398
1399 Returns None if it is not set.
1400 """
tandrii5d48c322016-08-18 16:19:37 -07001401 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001402
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001403 def GetGitSvnRemoteUrl(self):
1404 """Return the configured git-svn remote URL parsed from git svn info.
1405
1406 Returns None if it is not set.
1407 """
1408 # URL is dependent on the current directory.
1409 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1410 if data:
1411 keys = dict(line.split(': ', 1) for line in data.splitlines()
1412 if ': ' in line)
1413 return keys.get('URL', None)
1414 return None
1415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 def GetRemoteUrl(self):
1417 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1418
1419 Returns None if there is no remote.
1420 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001421 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001422 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1423
1424 # If URL is pointing to a local directory, it is probably a git cache.
1425 if os.path.isdir(url):
1426 url = RunGit(['config', 'remote.%s.url' % remote],
1427 error_ok=True,
1428 cwd=url).strip()
1429 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001431 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001432 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001433 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001434 self.issue = self._GitGetBranchConfigValue(
1435 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001436 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 return self.issue
1438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 def GetIssueURL(self):
1440 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001441 issue = self.GetIssue()
1442 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001443 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001444 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445
1446 def GetDescription(self, pretty=False):
1447 if not self.has_description:
1448 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001449 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 self.has_description = True
1451 if pretty:
1452 wrapper = textwrap.TextWrapper()
1453 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1454 return wrapper.fill(self.description)
1455 return self.description
1456
1457 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001458 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001459 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001460 self.patchset = self._GitGetBranchConfigValue(
1461 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001462 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 return self.patchset
1464
1465 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001466 """Set this branch's patchset. If patchset=0, clears the patchset."""
1467 assert self.GetBranch()
1468 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001469 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001470 else:
1471 self.patchset = int(patchset)
1472 self._GitSetBranchConfigValue(
1473 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001475 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001476 """Set this branch's issue. If issue isn't given, clears the issue."""
1477 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001479 issue = int(issue)
1480 self._GitSetBranchConfigValue(
1481 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001482 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001483 codereview_server = self._codereview_impl.GetCodereviewServer()
1484 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001485 self._GitSetBranchConfigValue(
1486 self._codereview_impl.CodereviewServerConfigKey(),
1487 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 else:
tandrii5d48c322016-08-18 16:19:37 -07001489 # Reset all of these just to be clean.
1490 reset_suffixes = [
1491 'last-upload-hash',
1492 self._codereview_impl.IssueConfigKey(),
1493 self._codereview_impl.PatchsetConfigKey(),
1494 self._codereview_impl.CodereviewServerConfigKey(),
1495 ] + self._PostUnsetIssueProperties()
1496 for prop in reset_suffixes:
1497 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001498 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001499 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
dnjba1b0f32016-09-02 12:37:42 -07001501 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001502 if not self.GitSanityChecks(upstream_branch):
1503 DieWithError('\nGit sanity check failure')
1504
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001505 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001506 if not root:
1507 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001508 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001509
1510 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001511 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001512 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001513 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001514 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001515 except subprocess2.CalledProcessError:
1516 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001517 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001518 'This branch probably doesn\'t exist anymore. To reset the\n'
1519 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001520 ' git branch --set-upstream-to origin/master %s\n'
1521 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001522 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001523
maruel@chromium.org52424302012-08-29 15:14:30 +00001524 issue = self.GetIssue()
1525 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001526 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001527 description = self.GetDescription()
1528 else:
1529 # If the change was never uploaded, use the log messages of all commits
1530 # up to the branch point, as git cl upload will prefill the description
1531 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001532 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1533 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001534
1535 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001536 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001537 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001538 name,
1539 description,
1540 absroot,
1541 files,
1542 issue,
1543 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001544 author,
1545 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001546
dsansomee2d6fd92016-09-08 00:10:47 -07001547 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001548 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001549 return self._codereview_impl.UpdateDescriptionRemote(
1550 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551
1552 def RunHook(self, committing, may_prompt, verbose, change):
1553 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1554 try:
1555 return presubmit_support.DoPresubmitChecks(change, committing,
1556 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1557 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001558 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1559 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001560 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561 DieWithError(
1562 ('%s\nMaybe your depot_tools is out of date?\n'
1563 'If all fails, contact maruel@') % e)
1564
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001565 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1566 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001567 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1568 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001569 else:
1570 # Assume url.
1571 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1572 urlparse.urlparse(issue_arg))
1573 if not parsed_issue_arg or not parsed_issue_arg.valid:
1574 DieWithError('Failed to parse issue argument "%s". '
1575 'Must be an issue number or a valid URL.' % issue_arg)
1576 return self._codereview_impl.CMDPatchWithParsedIssue(
1577 parsed_issue_arg, reject, nocommit, directory)
1578
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001579 def CMDUpload(self, options, git_diff_args, orig_args):
1580 """Uploads a change to codereview."""
1581 if git_diff_args:
1582 # TODO(ukai): is it ok for gerrit case?
1583 base_branch = git_diff_args[0]
1584 else:
1585 if self.GetBranch() is None:
1586 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1587
1588 # Default to diffing against common ancestor of upstream branch
1589 base_branch = self.GetCommonAncestorWithUpstream()
1590 git_diff_args = [base_branch, 'HEAD']
1591
1592 # Make sure authenticated to codereview before running potentially expensive
1593 # hooks. It is a fast, best efforts check. Codereview still can reject the
1594 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001595 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596
1597 # Apply watchlists on upload.
1598 change = self.GetChange(base_branch, None)
1599 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1600 files = [f.LocalPath() for f in change.AffectedFiles()]
1601 if not options.bypass_watchlists:
1602 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1603
1604 if not options.bypass_hooks:
1605 if options.reviewers or options.tbr_owners:
1606 # Set the reviewer list now so that presubmit checks can access it.
1607 change_description = ChangeDescription(change.FullDescriptionText())
1608 change_description.update_reviewers(options.reviewers,
1609 options.tbr_owners,
1610 change)
1611 change.SetDescriptionText(change_description.description)
1612 hook_results = self.RunHook(committing=False,
1613 may_prompt=not options.force,
1614 verbose=options.verbose,
1615 change=change)
1616 if not hook_results.should_continue():
1617 return 1
1618 if not options.reviewers and hook_results.reviewers:
1619 options.reviewers = hook_results.reviewers.split(',')
1620
1621 if self.GetIssue():
1622 latest_patchset = self.GetMostRecentPatchset()
1623 local_patchset = self.GetPatchset()
1624 if (latest_patchset and local_patchset and
1625 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001626 print('The last upload made from this repository was patchset #%d but '
1627 'the most recent patchset on the server is #%d.'
1628 % (local_patchset, latest_patchset))
1629 print('Uploading will still work, but if you\'ve uploaded to this '
1630 'issue from another machine or branch the patch you\'re '
1631 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001632 ask_for_data('About to upload; enter to confirm.')
1633
1634 print_stats(options.similarity, options.find_copies, git_diff_args)
1635 ret = self.CMDUploadChange(options, git_diff_args, change)
1636 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001637 if options.use_commit_queue:
1638 self.SetCQState(_CQState.COMMIT)
1639 elif options.cq_dry_run:
1640 self.SetCQState(_CQState.DRY_RUN)
1641
tandrii5d48c322016-08-18 16:19:37 -07001642 _git_set_branch_config_value('last-upload-hash',
1643 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644 # Run post upload hooks, if specified.
1645 if settings.GetRunPostUploadHook():
1646 presubmit_support.DoPostUploadExecuter(
1647 change,
1648 self,
1649 settings.GetRoot(),
1650 options.verbose,
1651 sys.stdout)
1652
1653 # Upload all dependencies if specified.
1654 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001655 print()
1656 print('--dependencies has been specified.')
1657 print('All dependent local branches will be re-uploaded.')
1658 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001659 # Remove the dependencies flag from args so that we do not end up in a
1660 # loop.
1661 orig_args.remove('--dependencies')
1662 ret = upload_branch_deps(self, orig_args)
1663 return ret
1664
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001665 def SetCQState(self, new_state):
1666 """Update the CQ state for latest patchset.
1667
1668 Issue must have been already uploaded and known.
1669 """
1670 assert new_state in _CQState.ALL_STATES
1671 assert self.GetIssue()
1672 return self._codereview_impl.SetCQState(new_state)
1673
qyearsley1fdfcb62016-10-24 13:22:03 -07001674 def TriggerDryRun(self):
1675 """Triggers a dry run and prints a warning on failure."""
1676 # TODO(qyearsley): Either re-use this method in CMDset_commit
1677 # and CMDupload, or change CMDtry to trigger dry runs with
1678 # just SetCQState, and catch keyboard interrupt and other
1679 # errors in that method.
1680 try:
1681 self.SetCQState(_CQState.DRY_RUN)
1682 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1683 return 0
1684 except KeyboardInterrupt:
1685 raise
1686 except:
1687 print('WARNING: failed to trigger CQ Dry Run.\n'
1688 'Either:\n'
1689 ' * your project has no CQ\n'
1690 ' * you don\'t have permission to trigger Dry Run\n'
1691 ' * bug in this code (see stack trace below).\n'
1692 'Consider specifying which bots to trigger manually '
1693 'or asking your project owners for permissions '
1694 'or contacting Chrome Infrastructure team at '
1695 'https://www.chromium.org/infra\n\n')
1696 # Still raise exception so that stack trace is printed.
1697 raise
1698
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001699 # Forward methods to codereview specific implementation.
1700
1701 def CloseIssue(self):
1702 return self._codereview_impl.CloseIssue()
1703
1704 def GetStatus(self):
1705 return self._codereview_impl.GetStatus()
1706
1707 def GetCodereviewServer(self):
1708 return self._codereview_impl.GetCodereviewServer()
1709
tandriide281ae2016-10-12 06:02:30 -07001710 def GetIssueOwner(self):
1711 """Get owner from codereview, which may differ from this checkout."""
1712 return self._codereview_impl.GetIssueOwner()
1713
1714 def GetIssueProject(self):
1715 """Get project from codereview, which may differ from what this
1716 checkout's codereview.settings or gerrit project URL say.
1717 """
1718 return self._codereview_impl.GetIssueProject()
1719
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001720 def GetApprovingReviewers(self):
1721 return self._codereview_impl.GetApprovingReviewers()
1722
1723 def GetMostRecentPatchset(self):
1724 return self._codereview_impl.GetMostRecentPatchset()
1725
tandriide281ae2016-10-12 06:02:30 -07001726 def CannotTriggerTryJobReason(self):
1727 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1728 return self._codereview_impl.CannotTriggerTryJobReason()
1729
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001730 def __getattr__(self, attr):
1731 # This is because lots of untested code accesses Rietveld-specific stuff
1732 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001733 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001734 # Note that child method defines __getattr__ as well, and forwards it here,
1735 # because _RietveldChangelistImpl is not cleaned up yet, and given
1736 # deprecation of Rietveld, it should probably be just removed.
1737 # Until that time, avoid infinite recursion by bypassing __getattr__
1738 # of implementation class.
1739 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740
1741
1742class _ChangelistCodereviewBase(object):
1743 """Abstract base class encapsulating codereview specifics of a changelist."""
1744 def __init__(self, changelist):
1745 self._changelist = changelist # instance of Changelist
1746
1747 def __getattr__(self, attr):
1748 # Forward methods to changelist.
1749 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1750 # _RietveldChangelistImpl to avoid this hack?
1751 return getattr(self._changelist, attr)
1752
1753 def GetStatus(self):
1754 """Apply a rough heuristic to give a simple summary of an issue's review
1755 or CQ status, assuming adherence to a common workflow.
1756
1757 Returns None if no issue for this branch, or specific string keywords.
1758 """
1759 raise NotImplementedError()
1760
1761 def GetCodereviewServer(self):
1762 """Returns server URL without end slash, like "https://codereview.com"."""
1763 raise NotImplementedError()
1764
1765 def FetchDescription(self):
1766 """Fetches and returns description from the codereview server."""
1767 raise NotImplementedError()
1768
tandrii5d48c322016-08-18 16:19:37 -07001769 @classmethod
1770 def IssueConfigKey(cls):
1771 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 raise NotImplementedError()
1773
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001774 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001775 def PatchsetConfigKey(cls):
1776 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001777 raise NotImplementedError()
1778
tandrii5d48c322016-08-18 16:19:37 -07001779 @classmethod
1780 def CodereviewServerConfigKey(cls):
1781 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 raise NotImplementedError()
1783
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001784 def _PostUnsetIssueProperties(self):
1785 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001786 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001787
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 def GetRieveldObjForPresubmit(self):
1789 # This is an unfortunate Rietveld-embeddedness in presubmit.
1790 # For non-Rietveld codereviews, this probably should return a dummy object.
1791 raise NotImplementedError()
1792
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001793 def GetGerritObjForPresubmit(self):
1794 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1795 return None
1796
dsansomee2d6fd92016-09-08 00:10:47 -07001797 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798 """Update the description on codereview site."""
1799 raise NotImplementedError()
1800
1801 def CloseIssue(self):
1802 """Closes the issue."""
1803 raise NotImplementedError()
1804
1805 def GetApprovingReviewers(self):
1806 """Returns a list of reviewers approving the change.
1807
1808 Note: not necessarily committers.
1809 """
1810 raise NotImplementedError()
1811
1812 def GetMostRecentPatchset(self):
1813 """Returns the most recent patchset number from the codereview site."""
1814 raise NotImplementedError()
1815
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001816 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1817 directory):
1818 """Fetches and applies the issue.
1819
1820 Arguments:
1821 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1822 reject: if True, reject the failed patch instead of switching to 3-way
1823 merge. Rietveld only.
1824 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1825 only.
1826 directory: switch to directory before applying the patch. Rietveld only.
1827 """
1828 raise NotImplementedError()
1829
1830 @staticmethod
1831 def ParseIssueURL(parsed_url):
1832 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1833 failed."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001836 def EnsureAuthenticated(self, force):
1837 """Best effort check that user is authenticated with codereview server.
1838
1839 Arguments:
1840 force: whether to skip confirmation questions.
1841 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001842 raise NotImplementedError()
1843
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001844 def CMDUploadChange(self, options, args, change):
1845 """Uploads a change to codereview."""
1846 raise NotImplementedError()
1847
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001848 def SetCQState(self, new_state):
1849 """Update the CQ state for latest patchset.
1850
1851 Issue must have been already uploaded and known.
1852 """
1853 raise NotImplementedError()
1854
tandriie113dfd2016-10-11 10:20:12 -07001855 def CannotTriggerTryJobReason(self):
1856 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1857 raise NotImplementedError()
1858
tandriide281ae2016-10-12 06:02:30 -07001859 def GetIssueOwner(self):
1860 raise NotImplementedError()
1861
1862 def GetIssueProject(self):
1863 raise NotImplementedError()
1864
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865
1866class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1867 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1868 super(_RietveldChangelistImpl, self).__init__(changelist)
1869 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001870 if not rietveld_server:
1871 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872
1873 self._rietveld_server = rietveld_server
1874 self._auth_config = auth_config
1875 self._props = None
1876 self._rpc_server = None
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def GetCodereviewServer(self):
1879 if not self._rietveld_server:
1880 # If we're on a branch then get the server potentially associated
1881 # with that branch.
1882 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001883 self._rietveld_server = gclient_utils.UpgradeToHttps(
1884 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 if not self._rietveld_server:
1886 self._rietveld_server = settings.GetDefaultServerUrl()
1887 return self._rietveld_server
1888
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001889 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001890 """Best effort check that user is authenticated with Rietveld server."""
1891 if self._auth_config.use_oauth2:
1892 authenticator = auth.get_authenticator_for_host(
1893 self.GetCodereviewServer(), self._auth_config)
1894 if not authenticator.has_cached_credentials():
1895 raise auth.LoginRequiredError(self.GetCodereviewServer())
1896
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001897 def FetchDescription(self):
1898 issue = self.GetIssue()
1899 assert issue
1900 try:
1901 return self.RpcServer().get_description(issue).strip()
1902 except urllib2.HTTPError as e:
1903 if e.code == 404:
1904 DieWithError(
1905 ('\nWhile fetching the description for issue %d, received a '
1906 '404 (not found)\n'
1907 'error. It is likely that you deleted this '
1908 'issue on the server. If this is the\n'
1909 'case, please run\n\n'
1910 ' git cl issue 0\n\n'
1911 'to clear the association with the deleted issue. Then run '
1912 'this command again.') % issue)
1913 else:
1914 DieWithError(
1915 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1916 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001917 print('Warning: Failed to retrieve CL description due to network '
1918 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919 return ''
1920
1921 def GetMostRecentPatchset(self):
1922 return self.GetIssueProperties()['patchsets'][-1]
1923
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 def GetIssueProperties(self):
1925 if self._props is None:
1926 issue = self.GetIssue()
1927 if not issue:
1928 self._props = {}
1929 else:
1930 self._props = self.RpcServer().get_issue_properties(issue, True)
1931 return self._props
1932
tandriie113dfd2016-10-11 10:20:12 -07001933 def CannotTriggerTryJobReason(self):
1934 props = self.GetIssueProperties()
1935 if not props:
1936 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1937 if props.get('closed'):
1938 return 'CL %s is closed' % self.GetIssue()
1939 if props.get('private'):
1940 return 'CL %s is private' % self.GetIssue()
1941 return None
1942
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001943 def GetApprovingReviewers(self):
1944 return get_approving_reviewers(self.GetIssueProperties())
1945
tandriide281ae2016-10-12 06:02:30 -07001946 def GetIssueOwner(self):
1947 return (self.GetIssueProperties() or {}).get('owner_email')
1948
1949 def GetIssueProject(self):
1950 return (self.GetIssueProperties() or {}).get('project')
1951
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952 def AddComment(self, message):
1953 return self.RpcServer().add_comment(self.GetIssue(), message)
1954
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001955 def GetStatus(self):
1956 """Apply a rough heuristic to give a simple summary of an issue's review
1957 or CQ status, assuming adherence to a common workflow.
1958
1959 Returns None if no issue for this branch, or one of the following keywords:
1960 * 'error' - error from review tool (including deleted issues)
1961 * 'unsent' - not sent for review
1962 * 'waiting' - waiting for review
1963 * 'reply' - waiting for owner to reply to review
1964 * 'lgtm' - LGTM from at least one approved reviewer
1965 * 'commit' - in the commit queue
1966 * 'closed' - closed
1967 """
1968 if not self.GetIssue():
1969 return None
1970
1971 try:
1972 props = self.GetIssueProperties()
1973 except urllib2.HTTPError:
1974 return 'error'
1975
1976 if props.get('closed'):
1977 # Issue is closed.
1978 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001979 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001980 # Issue is in the commit queue.
1981 return 'commit'
1982
1983 try:
1984 reviewers = self.GetApprovingReviewers()
1985 except urllib2.HTTPError:
1986 return 'error'
1987
1988 if reviewers:
1989 # Was LGTM'ed.
1990 return 'lgtm'
1991
1992 messages = props.get('messages') or []
1993
tandrii9d2c7a32016-06-22 03:42:45 -07001994 # Skip CQ messages that don't require owner's action.
1995 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1996 if 'Dry run:' in messages[-1]['text']:
1997 messages.pop()
1998 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1999 # This message always follows prior messages from CQ,
2000 # so skip this too.
2001 messages.pop()
2002 else:
2003 # This is probably a CQ messages warranting user attention.
2004 break
2005
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002006 if not messages:
2007 # No message was sent.
2008 return 'unsent'
2009 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002010 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002011 return 'reply'
2012 return 'waiting'
2013
dsansomee2d6fd92016-09-08 00:10:47 -07002014 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002015 return self.RpcServer().update_description(
2016 self.GetIssue(), self.description)
2017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002019 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002021 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002022 return self.SetFlags({flag: value})
2023
2024 def SetFlags(self, flags):
2025 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002026 """
phajdan.jr68598232016-08-10 03:28:28 -07002027 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002028 try:
tandrii4b233bd2016-07-06 03:50:29 -07002029 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002030 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002031 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002032 if e.code == 404:
2033 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2034 if e.code == 403:
2035 DieWithError(
2036 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002037 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002038 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002040 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 """Returns an upload.RpcServer() to access this review's rietveld instance.
2042 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002043 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002044 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002046 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002047 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002049 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002050 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002051 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002052
tandrii5d48c322016-08-18 16:19:37 -07002053 @classmethod
2054 def PatchsetConfigKey(cls):
2055 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056
tandrii5d48c322016-08-18 16:19:37 -07002057 @classmethod
2058 def CodereviewServerConfigKey(cls):
2059 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002060
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002061 def GetRieveldObjForPresubmit(self):
2062 return self.RpcServer()
2063
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002064 def SetCQState(self, new_state):
2065 props = self.GetIssueProperties()
2066 if props.get('private'):
2067 DieWithError('Cannot set-commit on private issue')
2068
2069 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002070 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002071 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002072 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002073 else:
tandrii4b233bd2016-07-06 03:50:29 -07002074 assert new_state == _CQState.DRY_RUN
2075 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002076
2077
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002078 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2079 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002080 # PatchIssue should never be called with a dirty tree. It is up to the
2081 # caller to check this, but just in case we assert here since the
2082 # consequences of the caller not checking this could be dire.
2083 assert(not git_common.is_dirty_git_tree('apply'))
2084 assert(parsed_issue_arg.valid)
2085 self._changelist.issue = parsed_issue_arg.issue
2086 if parsed_issue_arg.hostname:
2087 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2088
skobes6468b902016-10-24 08:45:10 -07002089 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2090 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2091 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002092 try:
skobes6468b902016-10-24 08:45:10 -07002093 scm_obj.apply_patch(patchset_object)
2094 except Exception as e:
2095 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002096 return 1
2097
2098 # If we had an issue, commit the current state and register the issue.
2099 if not nocommit:
2100 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2101 'patch from issue %(i)s at patchset '
2102 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2103 % {'i': self.GetIssue(), 'p': patchset})])
2104 self.SetIssue(self.GetIssue())
2105 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002106 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002108 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002109 return 0
2110
2111 @staticmethod
2112 def ParseIssueURL(parsed_url):
2113 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2114 return None
wychen3c1c1722016-08-04 11:46:36 -07002115 # Rietveld patch: https://domain/<number>/#ps<patchset>
2116 match = re.match(r'/(\d+)/$', parsed_url.path)
2117 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2118 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002119 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002120 issue=int(match.group(1)),
2121 patchset=int(match2.group(1)),
2122 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002123 # Typical url: https://domain/<issue_number>[/[other]]
2124 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2125 if match:
skobes6468b902016-10-24 08:45:10 -07002126 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002127 issue=int(match.group(1)),
2128 hostname=parsed_url.netloc)
2129 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2130 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2131 if match:
skobes6468b902016-10-24 08:45:10 -07002132 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002133 issue=int(match.group(1)),
2134 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002135 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002136 return None
2137
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002138 def CMDUploadChange(self, options, args, change):
2139 """Upload the patch to Rietveld."""
2140 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2141 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002142 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2143 if options.emulate_svn_auto_props:
2144 upload_args.append('--emulate_svn_auto_props')
2145
2146 change_desc = None
2147
2148 if options.email is not None:
2149 upload_args.extend(['--email', options.email])
2150
2151 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002152 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002153 upload_args.extend(['--title', options.title])
2154 if options.message:
2155 upload_args.extend(['--message', options.message])
2156 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print('This branch is associated with issue %s. '
2158 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002159 else:
nodirca166002016-06-27 10:59:51 -07002160 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002161 upload_args.extend(['--title', options.title])
2162 message = (options.title or options.message or
2163 CreateDescriptionFromLog(args))
2164 change_desc = ChangeDescription(message)
2165 if options.reviewers or options.tbr_owners:
2166 change_desc.update_reviewers(options.reviewers,
2167 options.tbr_owners,
2168 change)
2169 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002170 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002171
2172 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002173 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002174 return 1
2175
2176 upload_args.extend(['--message', change_desc.description])
2177 if change_desc.get_reviewers():
2178 upload_args.append('--reviewers=%s' % ','.join(
2179 change_desc.get_reviewers()))
2180 if options.send_mail:
2181 if not change_desc.get_reviewers():
2182 DieWithError("Must specify reviewers to send email.")
2183 upload_args.append('--send_mail')
2184
2185 # We check this before applying rietveld.private assuming that in
2186 # rietveld.cc only addresses which we can send private CLs to are listed
2187 # if rietveld.private is set, and so we should ignore rietveld.cc only
2188 # when --private is specified explicitly on the command line.
2189 if options.private:
2190 logging.warn('rietveld.cc is ignored since private flag is specified. '
2191 'You need to review and add them manually if necessary.')
2192 cc = self.GetCCListWithoutDefault()
2193 else:
2194 cc = self.GetCCList()
2195 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002196 if change_desc.get_cced():
2197 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198 if cc:
2199 upload_args.extend(['--cc', cc])
2200
2201 if options.private or settings.GetDefaultPrivateFlag() == "True":
2202 upload_args.append('--private')
2203
2204 upload_args.extend(['--git_similarity', str(options.similarity)])
2205 if not options.find_copies:
2206 upload_args.extend(['--git_no_find_copies'])
2207
2208 # Include the upstream repo's URL in the change -- this is useful for
2209 # projects that have their source spread across multiple repos.
2210 remote_url = self.GetGitBaseUrlFromConfig()
2211 if not remote_url:
2212 if settings.GetIsGitSvn():
2213 remote_url = self.GetGitSvnRemoteUrl()
2214 else:
2215 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2216 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2217 self.GetUpstreamBranch().split('/')[-1])
2218 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 remote, remote_branch = self.GetRemoteBranch()
2220 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2221 settings.GetPendingRefPrefix())
2222 if target_ref:
2223 upload_args.extend(['--target_ref', target_ref])
2224
2225 # Look for dependent patchsets. See crbug.com/480453 for more details.
2226 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2227 upstream_branch = ShortBranchName(upstream_branch)
2228 if remote is '.':
2229 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002230 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002232 print()
2233 print('Skipping dependency patchset upload because git config '
2234 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2235 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002236 else:
2237 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002238 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 auth_config=auth_config)
2240 branch_cl_issue_url = branch_cl.GetIssueURL()
2241 branch_cl_issue = branch_cl.GetIssue()
2242 branch_cl_patchset = branch_cl.GetPatchset()
2243 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2244 upload_args.extend(
2245 ['--depends_on_patchset', '%s:%s' % (
2246 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002247 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 '\n'
2249 'The current branch (%s) is tracking a local branch (%s) with '
2250 'an associated CL.\n'
2251 'Adding %s/#ps%s as a dependency patchset.\n'
2252 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2253 branch_cl_patchset))
2254
2255 project = settings.GetProject()
2256 if project:
2257 upload_args.extend(['--project', project])
2258
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002259 try:
2260 upload_args = ['upload'] + upload_args + args
2261 logging.info('upload.RealMain(%s)', upload_args)
2262 issue, patchset = upload.RealMain(upload_args)
2263 issue = int(issue)
2264 patchset = int(patchset)
2265 except KeyboardInterrupt:
2266 sys.exit(1)
2267 except:
2268 # If we got an exception after the user typed a description for their
2269 # change, back up the description before re-raising.
2270 if change_desc:
2271 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2272 print('\nGot exception while uploading -- saving description to %s\n' %
2273 backup_path)
2274 backup_file = open(backup_path, 'w')
2275 backup_file.write(change_desc.description)
2276 backup_file.close()
2277 raise
2278
2279 if not self.GetIssue():
2280 self.SetIssue(issue)
2281 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 return 0
2283
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002284
2285class _GerritChangelistImpl(_ChangelistCodereviewBase):
2286 def __init__(self, changelist, auth_config=None):
2287 # auth_config is Rietveld thing, kept here to preserve interface only.
2288 super(_GerritChangelistImpl, self).__init__(changelist)
2289 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002290 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002291 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002292 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002293
2294 def _GetGerritHost(self):
2295 # Lazy load of configs.
2296 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002297 if self._gerrit_host and '.' not in self._gerrit_host:
2298 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2299 # This happens for internal stuff http://crbug.com/614312.
2300 parsed = urlparse.urlparse(self.GetRemoteUrl())
2301 if parsed.scheme == 'sso':
2302 print('WARNING: using non https URLs for remote is likely broken\n'
2303 ' Your current remote is: %s' % self.GetRemoteUrl())
2304 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2305 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306 return self._gerrit_host
2307
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002308 def _GetGitHost(self):
2309 """Returns git host to be used when uploading change to Gerrit."""
2310 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2311
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002312 def GetCodereviewServer(self):
2313 if not self._gerrit_server:
2314 # If we're on a branch then get the server potentially associated
2315 # with that branch.
2316 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002317 self._gerrit_server = self._GitGetBranchConfigValue(
2318 self.CodereviewServerConfigKey())
2319 if self._gerrit_server:
2320 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321 if not self._gerrit_server:
2322 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2323 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002324 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002325 parts[0] = parts[0] + '-review'
2326 self._gerrit_host = '.'.join(parts)
2327 self._gerrit_server = 'https://%s' % self._gerrit_host
2328 return self._gerrit_server
2329
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002330 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002331 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002332 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333
tandrii5d48c322016-08-18 16:19:37 -07002334 @classmethod
2335 def PatchsetConfigKey(cls):
2336 return 'gerritpatchset'
2337
2338 @classmethod
2339 def CodereviewServerConfigKey(cls):
2340 return 'gerritserver'
2341
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002342 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002343 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002344 if settings.GetGerritSkipEnsureAuthenticated():
2345 # For projects with unusual authentication schemes.
2346 # See http://crbug.com/603378.
2347 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002348 # Lazy-loader to identify Gerrit and Git hosts.
2349 if gerrit_util.GceAuthenticator.is_gce():
2350 return
2351 self.GetCodereviewServer()
2352 git_host = self._GetGitHost()
2353 assert self._gerrit_server and self._gerrit_host
2354 cookie_auth = gerrit_util.CookiesAuthenticator()
2355
2356 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2357 git_auth = cookie_auth.get_auth_header(git_host)
2358 if gerrit_auth and git_auth:
2359 if gerrit_auth == git_auth:
2360 return
2361 print((
2362 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2363 ' Check your %s or %s file for credentials of hosts:\n'
2364 ' %s\n'
2365 ' %s\n'
2366 ' %s') %
2367 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2368 git_host, self._gerrit_host,
2369 cookie_auth.get_new_password_message(git_host)))
2370 if not force:
2371 ask_for_data('If you know what you are doing, press Enter to continue, '
2372 'Ctrl+C to abort.')
2373 return
2374 else:
2375 missing = (
2376 [] if gerrit_auth else [self._gerrit_host] +
2377 [] if git_auth else [git_host])
2378 DieWithError('Credentials for the following hosts are required:\n'
2379 ' %s\n'
2380 'These are read from %s (or legacy %s)\n'
2381 '%s' % (
2382 '\n '.join(missing),
2383 cookie_auth.get_gitcookies_path(),
2384 cookie_auth.get_netrc_path(),
2385 cookie_auth.get_new_password_message(git_host)))
2386
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002387 def _PostUnsetIssueProperties(self):
2388 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002389 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002390
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002391 def GetRieveldObjForPresubmit(self):
2392 class ThisIsNotRietveldIssue(object):
2393 def __nonzero__(self):
2394 # This is a hack to make presubmit_support think that rietveld is not
2395 # defined, yet still ensure that calls directly result in a decent
2396 # exception message below.
2397 return False
2398
2399 def __getattr__(self, attr):
2400 print(
2401 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2402 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2403 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2404 'or use Rietveld for codereview.\n'
2405 'See also http://crbug.com/579160.' % attr)
2406 raise NotImplementedError()
2407 return ThisIsNotRietveldIssue()
2408
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002409 def GetGerritObjForPresubmit(self):
2410 return presubmit_support.GerritAccessor(self._GetGerritHost())
2411
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002412 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002413 """Apply a rough heuristic to give a simple summary of an issue's review
2414 or CQ status, assuming adherence to a common workflow.
2415
2416 Returns None if no issue for this branch, or one of the following keywords:
2417 * 'error' - error from review tool (including deleted issues)
2418 * 'unsent' - no reviewers added
2419 * 'waiting' - waiting for review
2420 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002421 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2422 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002423 * 'commit' - in the commit queue
2424 * 'closed' - abandoned
2425 """
2426 if not self.GetIssue():
2427 return None
2428
2429 try:
2430 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002431 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002432 return 'error'
2433
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002434 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002435 return 'closed'
2436
2437 cq_label = data['labels'].get('Commit-Queue', {})
2438 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002439 votes = cq_label.get('all', [])
2440 highest_vote = 0
2441 for v in votes:
2442 highest_vote = max(highest_vote, v.get('value', 0))
2443 vote_value = str(highest_vote)
2444 if vote_value != '0':
2445 # Add a '+' if the value is not 0 to match the values in the label.
2446 # The cq_label does not have negatives.
2447 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002448 vote_text = cq_label.get('values', {}).get(vote_value, '')
2449 if vote_text.lower() == 'commit':
2450 return 'commit'
2451
2452 lgtm_label = data['labels'].get('Code-Review', {})
2453 if lgtm_label:
2454 if 'rejected' in lgtm_label:
2455 return 'not lgtm'
2456 if 'approved' in lgtm_label:
2457 return 'lgtm'
2458
2459 if not data.get('reviewers', {}).get('REVIEWER', []):
2460 return 'unsent'
2461
2462 messages = data.get('messages', [])
2463 if messages:
2464 owner = data['owner'].get('_account_id')
2465 last_message_author = messages[-1].get('author', {}).get('_account_id')
2466 if owner != last_message_author:
2467 # Some reply from non-owner.
2468 return 'reply'
2469
2470 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002471
2472 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002473 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002474 return data['revisions'][data['current_revision']]['_number']
2475
2476 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002477 data = self._GetChangeDetail(['CURRENT_REVISION'])
2478 current_rev = data['current_revision']
2479 url = data['revisions'][current_rev]['fetch']['http']['url']
2480 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002481
dsansomee2d6fd92016-09-08 00:10:47 -07002482 def UpdateDescriptionRemote(self, description, force=False):
2483 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2484 if not force:
2485 ask_for_data(
2486 'The description cannot be modified while the issue has a pending '
2487 'unpublished edit. Either publish the edit in the Gerrit web UI '
2488 'or delete it.\n\n'
2489 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2490
2491 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2492 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002493 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2494 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002495
2496 def CloseIssue(self):
2497 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2498
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002499 def GetApprovingReviewers(self):
2500 """Returns a list of reviewers approving the change.
2501
2502 Note: not necessarily committers.
2503 """
2504 raise NotImplementedError()
2505
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002506 def SubmitIssue(self, wait_for_merge=True):
2507 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2508 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002509
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002510 def _GetChangeDetail(self, options=None, issue=None):
2511 options = options or []
2512 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002513 assert issue, 'issue is required to query Gerrit'
2514 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002515 options)
tandriic2405f52016-10-10 08:13:15 -07002516 if not data:
2517 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2518 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002519
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520 def CMDLand(self, force, bypass_hooks, verbose):
2521 if git_common.is_dirty_git_tree('land'):
2522 return 1
tandriid60367b2016-06-22 05:25:12 -07002523 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2524 if u'Commit-Queue' in detail.get('labels', {}):
2525 if not force:
2526 ask_for_data('\nIt seems this repository has a Commit Queue, '
2527 'which can test and land changes for you. '
2528 'Are you sure you wish to bypass it?\n'
2529 'Press Enter to continue, Ctrl+C to abort.')
2530
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002531 differs = True
tandriic4344b52016-08-29 06:04:54 -07002532 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002533 # Note: git diff outputs nothing if there is no diff.
2534 if not last_upload or RunGit(['diff', last_upload]).strip():
2535 print('WARNING: some changes from local branch haven\'t been uploaded')
2536 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002537 if detail['current_revision'] == last_upload:
2538 differs = False
2539 else:
2540 print('WARNING: local branch contents differ from latest uploaded '
2541 'patchset')
2542 if differs:
2543 if not force:
2544 ask_for_data(
2545 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2546 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2547 elif not bypass_hooks:
2548 hook_results = self.RunHook(
2549 committing=True,
2550 may_prompt=not force,
2551 verbose=verbose,
2552 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2553 if not hook_results.should_continue():
2554 return 1
2555
2556 self.SubmitIssue(wait_for_merge=True)
2557 print('Issue %s has been submitted.' % self.GetIssueURL())
2558 return 0
2559
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002560 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2561 directory):
2562 assert not reject
2563 assert not nocommit
2564 assert not directory
2565 assert parsed_issue_arg.valid
2566
2567 self._changelist.issue = parsed_issue_arg.issue
2568
2569 if parsed_issue_arg.hostname:
2570 self._gerrit_host = parsed_issue_arg.hostname
2571 self._gerrit_server = 'https://%s' % self._gerrit_host
2572
tandriic2405f52016-10-10 08:13:15 -07002573 try:
2574 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2575 except GerritIssueNotExists as e:
2576 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002577
2578 if not parsed_issue_arg.patchset:
2579 # Use current revision by default.
2580 revision_info = detail['revisions'][detail['current_revision']]
2581 patchset = int(revision_info['_number'])
2582 else:
2583 patchset = parsed_issue_arg.patchset
2584 for revision_info in detail['revisions'].itervalues():
2585 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2586 break
2587 else:
2588 DieWithError('Couldn\'t find patchset %i in issue %i' %
2589 (parsed_issue_arg.patchset, self.GetIssue()))
2590
2591 fetch_info = revision_info['fetch']['http']
2592 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2593 RunGit(['cherry-pick', 'FETCH_HEAD'])
2594 self.SetIssue(self.GetIssue())
2595 self.SetPatchset(patchset)
2596 print('Committed patch for issue %i pathset %i locally' %
2597 (self.GetIssue(), self.GetPatchset()))
2598 return 0
2599
2600 @staticmethod
2601 def ParseIssueURL(parsed_url):
2602 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2603 return None
2604 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2605 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2606 # Short urls like https://domain/<issue_number> can be used, but don't allow
2607 # specifying the patchset (you'd 404), but we allow that here.
2608 if parsed_url.path == '/':
2609 part = parsed_url.fragment
2610 else:
2611 part = parsed_url.path
2612 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2613 if match:
2614 return _ParsedIssueNumberArgument(
2615 issue=int(match.group(2)),
2616 patchset=int(match.group(4)) if match.group(4) else None,
2617 hostname=parsed_url.netloc)
2618 return None
2619
tandrii16e0b4e2016-06-07 10:34:28 -07002620 def _GerritCommitMsgHookCheck(self, offer_removal):
2621 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2622 if not os.path.exists(hook):
2623 return
2624 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2625 # custom developer made one.
2626 data = gclient_utils.FileRead(hook)
2627 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2628 return
2629 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002630 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002631 'and may interfere with it in subtle ways.\n'
2632 'We recommend you remove the commit-msg hook.')
2633 if offer_removal:
2634 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2635 if reply.lower().startswith('y'):
2636 gclient_utils.rm_file_or_tree(hook)
2637 print('Gerrit commit-msg hook removed.')
2638 else:
2639 print('OK, will keep Gerrit commit-msg hook in place.')
2640
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 def CMDUploadChange(self, options, args, change):
2642 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002643 if options.squash and options.no_squash:
2644 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002645
2646 if not options.squash and not options.no_squash:
2647 # Load default for user, repo, squash=true, in this order.
2648 options.squash = settings.GetSquashGerritUploads()
2649 elif options.no_squash:
2650 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002651
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002652 # We assume the remote called "origin" is the one we want.
2653 # It is probably not worthwhile to support different workflows.
2654 gerrit_remote = 'origin'
2655
2656 remote, remote_branch = self.GetRemoteBranch()
2657 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2658 pending_prefix='')
2659
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002661 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 if self.GetIssue():
2663 # Try to get the message from a previous upload.
2664 message = self.GetDescription()
2665 if not message:
2666 DieWithError(
2667 'failed to fetch description from current Gerrit issue %d\n'
2668 '%s' % (self.GetIssue(), self.GetIssueURL()))
2669 change_id = self._GetChangeDetail()['change_id']
2670 while True:
2671 footer_change_ids = git_footers.get_footer_change_id(message)
2672 if footer_change_ids == [change_id]:
2673 break
2674 if not footer_change_ids:
2675 message = git_footers.add_footer_change_id(message, change_id)
2676 print('WARNING: appended missing Change-Id to issue description')
2677 continue
2678 # There is already a valid footer but with different or several ids.
2679 # Doing this automatically is non-trivial as we don't want to lose
2680 # existing other footers, yet we want to append just 1 desired
2681 # Change-Id. Thus, just create a new footer, but let user verify the
2682 # new description.
2683 message = '%s\n\nChange-Id: %s' % (message, change_id)
2684 print(
2685 'WARNING: issue %s has Change-Id footer(s):\n'
2686 ' %s\n'
2687 'but issue has Change-Id %s, according to Gerrit.\n'
2688 'Please, check the proposed correction to the description, '
2689 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2690 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2691 change_id))
2692 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2693 if not options.force:
2694 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002695 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002696 message = change_desc.description
2697 if not message:
2698 DieWithError("Description is empty. Aborting...")
2699 # Continue the while loop.
2700 # Sanity check of this code - we should end up with proper message
2701 # footer.
2702 assert [change_id] == git_footers.get_footer_change_id(message)
2703 change_desc = ChangeDescription(message)
2704 else:
2705 change_desc = ChangeDescription(
2706 options.message or CreateDescriptionFromLog(args))
2707 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002708 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 if not change_desc.description:
2710 DieWithError("Description is empty. Aborting...")
2711 message = change_desc.description
2712 change_ids = git_footers.get_footer_change_id(message)
2713 if len(change_ids) > 1:
2714 DieWithError('too many Change-Id footers, at most 1 allowed.')
2715 if not change_ids:
2716 # Generate the Change-Id automatically.
2717 message = git_footers.add_footer_change_id(
2718 message, GenerateGerritChangeId(message))
2719 change_desc.set_description(message)
2720 change_ids = git_footers.get_footer_change_id(message)
2721 assert len(change_ids) == 1
2722 change_id = change_ids[0]
2723
2724 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2725 if remote is '.':
2726 # If our upstream branch is local, we base our squashed commit on its
2727 # squashed version.
2728 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2729 # Check the squashed hash of the parent.
2730 parent = RunGit(['config',
2731 'branch.%s.gerritsquashhash' % upstream_branch_name],
2732 error_ok=True).strip()
2733 # Verify that the upstream branch has been uploaded too, otherwise
2734 # Gerrit will create additional CLs when uploading.
2735 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2736 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 DieWithError(
2738 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002739 'Note: maybe you\'ve uploaded it with --no-squash. '
2740 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 ' git cl upload --squash\n' % upstream_branch_name)
2742 else:
2743 parent = self.GetCommonAncestorWithUpstream()
2744
2745 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2746 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2747 '-m', message]).strip()
2748 else:
2749 change_desc = ChangeDescription(
2750 options.message or CreateDescriptionFromLog(args))
2751 if not change_desc.description:
2752 DieWithError("Description is empty. Aborting...")
2753
2754 if not git_footers.get_footer_change_id(change_desc.description):
2755 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002756 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2757 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002758 ref_to_push = 'HEAD'
2759 parent = '%s/%s' % (gerrit_remote, branch)
2760 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2761
2762 assert change_desc
2763 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2764 ref_to_push)]).splitlines()
2765 if len(commits) > 1:
2766 print('WARNING: This will upload %d commits. Run the following command '
2767 'to see which commits will be uploaded: ' % len(commits))
2768 print('git log %s..%s' % (parent, ref_to_push))
2769 print('You can also use `git squash-branch` to squash these into a '
2770 'single commit.')
2771 ask_for_data('About to upload; enter to confirm.')
2772
2773 if options.reviewers or options.tbr_owners:
2774 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2775 change)
2776
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002777 # Extra options that can be specified at push time. Doc:
2778 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2779 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002780 if change_desc.get_reviewers(tbr_only=True):
2781 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2782 refspec_opts.append('l=Code-Review+1')
2783
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002784 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002785 if not re.match(r'^[\w ]+$', options.title):
2786 options.title = re.sub(r'[^\w ]', '', options.title)
2787 print('WARNING: Patchset title may only contain alphanumeric chars '
2788 'and spaces. Cleaned up title:\n%s' % options.title)
2789 if not options.force:
2790 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002791 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2792 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002793 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2794
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002795 if options.send_mail:
2796 if not change_desc.get_reviewers():
2797 DieWithError('Must specify reviewers to send email.')
2798 refspec_opts.append('notify=ALL')
2799 else:
2800 refspec_opts.append('notify=NONE')
2801
tandrii99a72f22016-08-17 14:33:24 -07002802 reviewers = change_desc.get_reviewers()
2803 if reviewers:
2804 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002805
agablec6787972016-09-09 16:13:34 -07002806 if options.private:
2807 refspec_opts.append('draft')
2808
rmistry9eadede2016-09-19 11:22:43 -07002809 if options.topic:
2810 # Documentation on Gerrit topics is here:
2811 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2812 refspec_opts.append('topic=%s' % options.topic)
2813
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002814 refspec_suffix = ''
2815 if refspec_opts:
2816 refspec_suffix = '%' + ','.join(refspec_opts)
2817 assert ' ' not in refspec_suffix, (
2818 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002819 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002820
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002822 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823 print_stdout=True,
2824 # Flush after every line: useful for seeing progress when running as
2825 # recipe.
2826 filter_fn=lambda _: sys.stdout.flush())
2827
2828 if options.squash:
2829 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2830 change_numbers = [m.group(1)
2831 for m in map(regex.match, push_stdout.splitlines())
2832 if m]
2833 if len(change_numbers) != 1:
2834 DieWithError(
2835 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2836 'Change-Id: %s') % (len(change_numbers), change_id))
2837 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002838 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002839
2840 # Add cc's from the CC_LIST and --cc flag (if any).
2841 cc = self.GetCCList().split(',')
2842 if options.cc:
2843 cc.extend(options.cc)
2844 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002845 if change_desc.get_cced():
2846 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002847 if cc:
2848 gerrit_util.AddReviewers(
2849 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2850
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002851 return 0
2852
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002853 def _AddChangeIdToCommitMessage(self, options, args):
2854 """Re-commits using the current message, assumes the commit hook is in
2855 place.
2856 """
2857 log_desc = options.message or CreateDescriptionFromLog(args)
2858 git_command = ['commit', '--amend', '-m', log_desc]
2859 RunGit(git_command)
2860 new_log_desc = CreateDescriptionFromLog(args)
2861 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002862 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002863 return new_log_desc
2864 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002865 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002866
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002867 def SetCQState(self, new_state):
2868 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002869 vote_map = {
2870 _CQState.NONE: 0,
2871 _CQState.DRY_RUN: 1,
2872 _CQState.COMMIT : 2,
2873 }
2874 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2875 labels={'Commit-Queue': vote_map[new_state]})
2876
tandriie113dfd2016-10-11 10:20:12 -07002877 def CannotTriggerTryJobReason(self):
2878 # TODO(tandrii): implement for Gerrit.
2879 raise NotImplementedError()
2880
tandriide281ae2016-10-12 06:02:30 -07002881 def GetIssueOwner(self):
2882 # TODO(tandrii): implement for Gerrit.
2883 raise NotImplementedError()
2884
2885 def GetIssueProject(self):
2886 # TODO(tandrii): implement for Gerrit.
2887 raise NotImplementedError()
2888
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002889
2890_CODEREVIEW_IMPLEMENTATIONS = {
2891 'rietveld': _RietveldChangelistImpl,
2892 'gerrit': _GerritChangelistImpl,
2893}
2894
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002895
iannuccie53c9352016-08-17 14:40:40 -07002896def _add_codereview_issue_select_options(parser, extra=""):
2897 _add_codereview_select_options(parser)
2898
2899 text = ('Operate on this issue number instead of the current branch\'s '
2900 'implicit issue.')
2901 if extra:
2902 text += ' '+extra
2903 parser.add_option('-i', '--issue', type=int, help=text)
2904
2905
2906def _process_codereview_issue_select_options(parser, options):
2907 _process_codereview_select_options(parser, options)
2908 if options.issue is not None and not options.forced_codereview:
2909 parser.error('--issue must be specified with either --rietveld or --gerrit')
2910
2911
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002912def _add_codereview_select_options(parser):
2913 """Appends --gerrit and --rietveld options to force specific codereview."""
2914 parser.codereview_group = optparse.OptionGroup(
2915 parser, 'EXPERIMENTAL! Codereview override options')
2916 parser.add_option_group(parser.codereview_group)
2917 parser.codereview_group.add_option(
2918 '--gerrit', action='store_true',
2919 help='Force the use of Gerrit for codereview')
2920 parser.codereview_group.add_option(
2921 '--rietveld', action='store_true',
2922 help='Force the use of Rietveld for codereview')
2923
2924
2925def _process_codereview_select_options(parser, options):
2926 if options.gerrit and options.rietveld:
2927 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2928 options.forced_codereview = None
2929 if options.gerrit:
2930 options.forced_codereview = 'gerrit'
2931 elif options.rietveld:
2932 options.forced_codereview = 'rietveld'
2933
2934
tandriif9aefb72016-07-01 09:06:51 -07002935def _get_bug_line_values(default_project, bugs):
2936 """Given default_project and comma separated list of bugs, yields bug line
2937 values.
2938
2939 Each bug can be either:
2940 * a number, which is combined with default_project
2941 * string, which is left as is.
2942
2943 This function may produce more than one line, because bugdroid expects one
2944 project per line.
2945
2946 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2947 ['v8:123', 'chromium:789']
2948 """
2949 default_bugs = []
2950 others = []
2951 for bug in bugs.split(','):
2952 bug = bug.strip()
2953 if bug:
2954 try:
2955 default_bugs.append(int(bug))
2956 except ValueError:
2957 others.append(bug)
2958
2959 if default_bugs:
2960 default_bugs = ','.join(map(str, default_bugs))
2961 if default_project:
2962 yield '%s:%s' % (default_project, default_bugs)
2963 else:
2964 yield default_bugs
2965 for other in sorted(others):
2966 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2967 yield other
2968
2969
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002970class ChangeDescription(object):
2971 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002972 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002973 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002974 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002975
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002976 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002977 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002978
agable@chromium.org42c20792013-09-12 17:34:49 +00002979 @property # www.logilab.org/ticket/89786
2980 def description(self): # pylint: disable=E0202
2981 return '\n'.join(self._description_lines)
2982
2983 def set_description(self, desc):
2984 if isinstance(desc, basestring):
2985 lines = desc.splitlines()
2986 else:
2987 lines = [line.rstrip() for line in desc]
2988 while lines and not lines[0]:
2989 lines.pop(0)
2990 while lines and not lines[-1]:
2991 lines.pop(-1)
2992 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002993
piman@chromium.org336f9122014-09-04 02:16:55 +00002994 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002995 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002996 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002997 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002998 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002999 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003000
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 # Get the set of R= and TBR= lines and remove them from the desciption.
3002 regexp = re.compile(self.R_LINE)
3003 matches = [regexp.match(line) for line in self._description_lines]
3004 new_desc = [l for i, l in enumerate(self._description_lines)
3005 if not matches[i]]
3006 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003007
agable@chromium.org42c20792013-09-12 17:34:49 +00003008 # Construct new unified R= and TBR= lines.
3009 r_names = []
3010 tbr_names = []
3011 for match in matches:
3012 if not match:
3013 continue
3014 people = cleanup_list([match.group(2).strip()])
3015 if match.group(1) == 'TBR':
3016 tbr_names.extend(people)
3017 else:
3018 r_names.extend(people)
3019 for name in r_names:
3020 if name not in reviewers:
3021 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003022 if add_owners_tbr:
3023 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003024 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003025 all_reviewers = set(tbr_names + reviewers)
3026 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3027 all_reviewers)
3028 tbr_names.extend(owners_db.reviewers_for(missing_files,
3029 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003030 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3031 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3032
3033 # Put the new lines in the description where the old first R= line was.
3034 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3035 if 0 <= line_loc < len(self._description_lines):
3036 if new_tbr_line:
3037 self._description_lines.insert(line_loc, new_tbr_line)
3038 if new_r_line:
3039 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003040 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003041 if new_r_line:
3042 self.append_footer(new_r_line)
3043 if new_tbr_line:
3044 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003045
tandriif9aefb72016-07-01 09:06:51 -07003046 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003047 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003048 self.set_description([
3049 '# Enter a description of the change.',
3050 '# This will be displayed on the codereview site.',
3051 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003052 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003053 '--------------------',
3054 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003055
agable@chromium.org42c20792013-09-12 17:34:49 +00003056 regexp = re.compile(self.BUG_LINE)
3057 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003058 prefix = settings.GetBugPrefix()
3059 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3060 for value in values:
3061 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3062 self.append_footer('BUG=%s' % value)
3063
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003065 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003066 if not content:
3067 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003068 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069
3070 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003071 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3072 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003073 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003074 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003075
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003076 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003077 """Adds a footer line to the description.
3078
3079 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3080 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3081 that Gerrit footers are always at the end.
3082 """
3083 parsed_footer_line = git_footers.parse_footer(line)
3084 if parsed_footer_line:
3085 # Line is a gerrit footer in the form: Footer-Key: any value.
3086 # Thus, must be appended observing Gerrit footer rules.
3087 self.set_description(
3088 git_footers.add_footer(self.description,
3089 key=parsed_footer_line[0],
3090 value=parsed_footer_line[1]))
3091 return
3092
3093 if not self._description_lines:
3094 self._description_lines.append(line)
3095 return
3096
3097 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3098 if gerrit_footers:
3099 # git_footers.split_footers ensures that there is an empty line before
3100 # actual (gerrit) footers, if any. We have to keep it that way.
3101 assert top_lines and top_lines[-1] == ''
3102 top_lines, separator = top_lines[:-1], top_lines[-1:]
3103 else:
3104 separator = [] # No need for separator if there are no gerrit_footers.
3105
3106 prev_line = top_lines[-1] if top_lines else ''
3107 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3108 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3109 top_lines.append('')
3110 top_lines.append(line)
3111 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003112
tandrii99a72f22016-08-17 14:33:24 -07003113 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003114 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003115 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003116 reviewers = [match.group(2).strip()
3117 for match in matches
3118 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003119 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003120
bradnelsond975b302016-10-23 12:20:23 -07003121 def get_cced(self):
3122 """Retrieves the list of reviewers."""
3123 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3124 cced = [match.group(2).strip() for match in matches if match]
3125 return cleanup_list(cced)
3126
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003127
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003128def get_approving_reviewers(props):
3129 """Retrieves the reviewers that approved a CL from the issue properties with
3130 messages.
3131
3132 Note that the list may contain reviewers that are not committer, thus are not
3133 considered by the CQ.
3134 """
3135 return sorted(
3136 set(
3137 message['sender']
3138 for message in props['messages']
3139 if message['approval'] and message['sender'] in props['reviewers']
3140 )
3141 )
3142
3143
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003144def FindCodereviewSettingsFile(filename='codereview.settings'):
3145 """Finds the given file starting in the cwd and going up.
3146
3147 Only looks up to the top of the repository unless an
3148 'inherit-review-settings-ok' file exists in the root of the repository.
3149 """
3150 inherit_ok_file = 'inherit-review-settings-ok'
3151 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003152 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003153 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3154 root = '/'
3155 while True:
3156 if filename in os.listdir(cwd):
3157 if os.path.isfile(os.path.join(cwd, filename)):
3158 return open(os.path.join(cwd, filename))
3159 if cwd == root:
3160 break
3161 cwd = os.path.dirname(cwd)
3162
3163
3164def LoadCodereviewSettingsFromFile(fileobj):
3165 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003166 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168 def SetProperty(name, setting, unset_error_ok=False):
3169 fullname = 'rietveld.' + name
3170 if setting in keyvals:
3171 RunGit(['config', fullname, keyvals[setting]])
3172 else:
3173 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3174
tandrii48df5812016-10-17 03:55:37 -07003175 if not keyvals.get('GERRIT_HOST', False):
3176 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003177 # Only server setting is required. Other settings can be absent.
3178 # In that case, we ignore errors raised during option deletion attempt.
3179 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003180 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3182 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003183 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003184 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003185 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3186 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003187 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003188 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003189 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003190 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003191 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3192 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003193
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003194 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003195 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003196
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003197 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003198 RunGit(['config', 'gerrit.squash-uploads',
3199 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003200
tandrii@chromium.org28253532016-04-14 13:46:56 +00003201 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003202 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003203 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3206 #should be of the form
3207 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3208 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3209 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3210 keyvals['ORIGIN_URL_CONFIG']])
3211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003212
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003213def urlretrieve(source, destination):
3214 """urllib is broken for SSL connections via a proxy therefore we
3215 can't use urllib.urlretrieve()."""
3216 with open(destination, 'w') as f:
3217 f.write(urllib2.urlopen(source).read())
3218
3219
ukai@chromium.org712d6102013-11-27 00:52:58 +00003220def hasSheBang(fname):
3221 """Checks fname is a #! script."""
3222 with open(fname) as f:
3223 return f.read(2).startswith('#!')
3224
3225
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003226# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3227def DownloadHooks(*args, **kwargs):
3228 pass
3229
3230
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003231def DownloadGerritHook(force):
3232 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003233
3234 Args:
3235 force: True to update hooks. False to install hooks if not present.
3236 """
3237 if not settings.GetIsGerrit():
3238 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003239 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003240 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3241 if not os.access(dst, os.X_OK):
3242 if os.path.exists(dst):
3243 if not force:
3244 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003245 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003246 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003247 if not hasSheBang(dst):
3248 DieWithError('Not a script: %s\n'
3249 'You need to download from\n%s\n'
3250 'into .git/hooks/commit-msg and '
3251 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003252 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3253 except Exception:
3254 if os.path.exists(dst):
3255 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003256 DieWithError('\nFailed to download hooks.\n'
3257 'You need to download from\n%s\n'
3258 'into .git/hooks/commit-msg and '
3259 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003260
3261
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003262
3263def GetRietveldCodereviewSettingsInteractively():
3264 """Prompt the user for settings."""
3265 server = settings.GetDefaultServerUrl(error_ok=True)
3266 prompt = 'Rietveld server (host[:port])'
3267 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3268 newserver = ask_for_data(prompt + ':')
3269 if not server and not newserver:
3270 newserver = DEFAULT_SERVER
3271 if newserver:
3272 newserver = gclient_utils.UpgradeToHttps(newserver)
3273 if newserver != server:
3274 RunGit(['config', 'rietveld.server', newserver])
3275
3276 def SetProperty(initial, caption, name, is_url):
3277 prompt = caption
3278 if initial:
3279 prompt += ' ("x" to clear) [%s]' % initial
3280 new_val = ask_for_data(prompt + ':')
3281 if new_val == 'x':
3282 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3283 elif new_val:
3284 if is_url:
3285 new_val = gclient_utils.UpgradeToHttps(new_val)
3286 if new_val != initial:
3287 RunGit(['config', 'rietveld.' + name, new_val])
3288
3289 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3290 SetProperty(settings.GetDefaultPrivateFlag(),
3291 'Private flag (rietveld only)', 'private', False)
3292 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3293 'tree-status-url', False)
3294 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3295 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3296 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3297 'run-post-upload-hook', False)
3298
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003299@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003300def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003301 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302
tandrii5d0a0422016-09-14 06:24:35 -07003303 print('WARNING: git cl config works for Rietveld only')
3304 # TODO(tandrii): remove this once we switch to Gerrit.
3305 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003306 parser.add_option('--activate-update', action='store_true',
3307 help='activate auto-updating [rietveld] section in '
3308 '.git/config')
3309 parser.add_option('--deactivate-update', action='store_true',
3310 help='deactivate auto-updating [rietveld] section in '
3311 '.git/config')
3312 options, args = parser.parse_args(args)
3313
3314 if options.deactivate_update:
3315 RunGit(['config', 'rietveld.autoupdate', 'false'])
3316 return
3317
3318 if options.activate_update:
3319 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3320 return
3321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003322 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003323 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003324 return 0
3325
3326 url = args[0]
3327 if not url.endswith('codereview.settings'):
3328 url = os.path.join(url, 'codereview.settings')
3329
3330 # Load code review settings and download hooks (if available).
3331 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3332 return 0
3333
3334
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003335def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003336 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003337 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3338 branch = ShortBranchName(branchref)
3339 _, args = parser.parse_args(args)
3340 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003341 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003342 return RunGit(['config', 'branch.%s.base-url' % branch],
3343 error_ok=False).strip()
3344 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003345 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003346 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3347 error_ok=False).strip()
3348
3349
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003350def color_for_status(status):
3351 """Maps a Changelist status to color, for CMDstatus and other tools."""
3352 return {
3353 'unsent': Fore.RED,
3354 'waiting': Fore.BLUE,
3355 'reply': Fore.YELLOW,
3356 'lgtm': Fore.GREEN,
3357 'commit': Fore.MAGENTA,
3358 'closed': Fore.CYAN,
3359 'error': Fore.WHITE,
3360 }.get(status, Fore.WHITE)
3361
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003362
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003363def get_cl_statuses(changes, fine_grained, max_processes=None):
3364 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003365
3366 If fine_grained is true, this will fetch CL statuses from the server.
3367 Otherwise, simply indicate if there's a matching url for the given branches.
3368
3369 If max_processes is specified, it is used as the maximum number of processes
3370 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3371 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003372
3373 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003374 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003375 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003376 upload.verbosity = 0
3377
3378 if fine_grained:
3379 # Process one branch synchronously to work through authentication, then
3380 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003381 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003382 def fetch(cl):
3383 try:
3384 return (cl, cl.GetStatus())
3385 except:
3386 # See http://crbug.com/629863.
3387 logging.exception('failed to fetch status for %s:', cl)
3388 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003389 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003390
tandriiea9514a2016-08-17 12:32:37 -07003391 changes_to_fetch = changes[1:]
3392 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003393 # Exit early if there was only one branch to fetch.
3394 return
3395
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003396 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003397 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003398 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003399 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003400
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003401 fetched_cls = set()
3402 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003403 while True:
3404 try:
3405 row = it.next(timeout=5)
3406 except multiprocessing.TimeoutError:
3407 break
3408
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003409 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003410 yield row
3411
3412 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003413 for cl in set(changes_to_fetch) - fetched_cls:
3414 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003415
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003416 else:
3417 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003418 for cl in changes:
3419 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003420
rmistry@google.com2dd99862015-06-22 12:22:18 +00003421
3422def upload_branch_deps(cl, args):
3423 """Uploads CLs of local branches that are dependents of the current branch.
3424
3425 If the local branch dependency tree looks like:
3426 test1 -> test2.1 -> test3.1
3427 -> test3.2
3428 -> test2.2 -> test3.3
3429
3430 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3431 run on the dependent branches in this order:
3432 test2.1, test3.1, test3.2, test2.2, test3.3
3433
3434 Note: This function does not rebase your local dependent branches. Use it when
3435 you make a change to the parent branch that will not conflict with its
3436 dependent branches, and you would like their dependencies updated in
3437 Rietveld.
3438 """
3439 if git_common.is_dirty_git_tree('upload-branch-deps'):
3440 return 1
3441
3442 root_branch = cl.GetBranch()
3443 if root_branch is None:
3444 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3445 'Get on a branch!')
3446 if not cl.GetIssue() or not cl.GetPatchset():
3447 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3448 'patchset dependencies without an uploaded CL.')
3449
3450 branches = RunGit(['for-each-ref',
3451 '--format=%(refname:short) %(upstream:short)',
3452 'refs/heads'])
3453 if not branches:
3454 print('No local branches found.')
3455 return 0
3456
3457 # Create a dictionary of all local branches to the branches that are dependent
3458 # on it.
3459 tracked_to_dependents = collections.defaultdict(list)
3460 for b in branches.splitlines():
3461 tokens = b.split()
3462 if len(tokens) == 2:
3463 branch_name, tracked = tokens
3464 tracked_to_dependents[tracked].append(branch_name)
3465
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print()
3467 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003468 dependents = []
3469 def traverse_dependents_preorder(branch, padding=''):
3470 dependents_to_process = tracked_to_dependents.get(branch, [])
3471 padding += ' '
3472 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003474 dependents.append(dependent)
3475 traverse_dependents_preorder(dependent, padding)
3476 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003477 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003478
3479 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003480 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003481 return 0
3482
vapiera7fbd5a2016-06-16 09:17:49 -07003483 print('This command will checkout all dependent branches and run '
3484 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003485 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3486
andybons@chromium.org962f9462016-02-03 20:00:42 +00003487 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003488 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003489 args.extend(['-t', 'Updated patchset dependency'])
3490
rmistry@google.com2dd99862015-06-22 12:22:18 +00003491 # Record all dependents that failed to upload.
3492 failures = {}
3493 # Go through all dependents, checkout the branch and upload.
3494 try:
3495 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003496 print()
3497 print('--------------------------------------')
3498 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003499 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003500 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003501 try:
3502 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003503 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003504 failures[dependent_branch] = 1
3505 except: # pylint: disable=W0702
3506 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003507 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003508 finally:
3509 # Swap back to the original root branch.
3510 RunGit(['checkout', '-q', root_branch])
3511
vapiera7fbd5a2016-06-16 09:17:49 -07003512 print()
3513 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003514 for dependent_branch in dependents:
3515 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003516 print(' %s : %s' % (dependent_branch, upload_status))
3517 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003518
3519 return 0
3520
3521
kmarshall3bff56b2016-06-06 18:31:47 -07003522def CMDarchive(parser, args):
3523 """Archives and deletes branches associated with closed changelists."""
3524 parser.add_option(
3525 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003526 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003527 parser.add_option(
3528 '-f', '--force', action='store_true',
3529 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003530 parser.add_option(
3531 '-d', '--dry-run', action='store_true',
3532 help='Skip the branch tagging and removal steps.')
3533 parser.add_option(
3534 '-t', '--notags', action='store_true',
3535 help='Do not tag archived branches. '
3536 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003537
3538 auth.add_auth_options(parser)
3539 options, args = parser.parse_args(args)
3540 if args:
3541 parser.error('Unsupported args: %s' % ' '.join(args))
3542 auth_config = auth.extract_auth_config_from_options(options)
3543
3544 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3545 if not branches:
3546 return 0
3547
vapiera7fbd5a2016-06-16 09:17:49 -07003548 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003549 changes = [Changelist(branchref=b, auth_config=auth_config)
3550 for b in branches.splitlines()]
3551 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3552 statuses = get_cl_statuses(changes,
3553 fine_grained=True,
3554 max_processes=options.maxjobs)
3555 proposal = [(cl.GetBranch(),
3556 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3557 for cl, status in statuses
3558 if status == 'closed']
3559 proposal.sort()
3560
3561 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003562 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003563 return 0
3564
3565 current_branch = GetCurrentBranch()
3566
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003568 if options.notags:
3569 for next_item in proposal:
3570 print(' ' + next_item[0])
3571 else:
3572 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3573 for next_item in proposal:
3574 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003575
kmarshall9249e012016-08-23 12:02:16 -07003576 # Quit now on precondition failure or if instructed by the user, either
3577 # via an interactive prompt or by command line flags.
3578 if options.dry_run:
3579 print('\nNo changes were made (dry run).\n')
3580 return 0
3581 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003582 print('You are currently on a branch \'%s\' which is associated with a '
3583 'closed codereview issue, so archive cannot proceed. Please '
3584 'checkout another branch and run this command again.' %
3585 current_branch)
3586 return 1
kmarshall9249e012016-08-23 12:02:16 -07003587 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003588 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3589 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003591 return 1
3592
3593 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003594 if not options.notags:
3595 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003596 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003597
vapiera7fbd5a2016-06-16 09:17:49 -07003598 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003599
3600 return 0
3601
3602
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003603def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003604 """Show status of changelists.
3605
3606 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003607 - Red not sent for review or broken
3608 - Blue waiting for review
3609 - Yellow waiting for you to reply to review
3610 - Green LGTM'ed
3611 - Magenta in the commit queue
3612 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003613
3614 Also see 'git cl comments'.
3615 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003617 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003618 parser.add_option('-f', '--fast', action='store_true',
3619 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003620 parser.add_option(
3621 '-j', '--maxjobs', action='store', type=int,
3622 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003623
3624 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003625 _add_codereview_issue_select_options(
3626 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003627 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003628 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003629 if args:
3630 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003631 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632
iannuccie53c9352016-08-17 14:40:40 -07003633 if options.issue is not None and not options.field:
3634 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003635
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003637 cl = Changelist(auth_config=auth_config, issue=options.issue,
3638 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003639 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003640 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641 elif options.field == 'id':
3642 issueid = cl.GetIssue()
3643 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003644 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645 elif options.field == 'patch':
3646 patchset = cl.GetPatchset()
3647 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003648 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003649 elif options.field == 'status':
3650 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003651 elif options.field == 'url':
3652 url = cl.GetIssueURL()
3653 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003654 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003655 return 0
3656
3657 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3658 if not branches:
3659 print('No local branch found.')
3660 return 0
3661
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003662 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003663 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003664 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003665 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003666 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003667 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003668 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003669
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003670 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003671 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3672 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3673 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003674 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003675 c, status = output.next()
3676 branch_statuses[c.GetBranch()] = status
3677 status = branch_statuses.pop(branch)
3678 url = cl.GetIssueURL()
3679 if url and (not status or status == 'error'):
3680 # The issue probably doesn't exist anymore.
3681 url += ' (broken)'
3682
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003683 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003684 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003685 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003686 color = ''
3687 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003688 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003689 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003690 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003691 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003692
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003693 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print()
3695 print('Current branch:',)
3696 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003697 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003698 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003699 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003700 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003701 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003702 print('Issue description:')
3703 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704 return 0
3705
3706
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003707def colorize_CMDstatus_doc():
3708 """To be called once in main() to add colors to git cl status help."""
3709 colors = [i for i in dir(Fore) if i[0].isupper()]
3710
3711 def colorize_line(line):
3712 for color in colors:
3713 if color in line.upper():
3714 # Extract whitespaces first and the leading '-'.
3715 indent = len(line) - len(line.lstrip(' ')) + 1
3716 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3717 return line
3718
3719 lines = CMDstatus.__doc__.splitlines()
3720 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3721
3722
phajdan.jre328cf92016-08-22 04:12:17 -07003723def write_json(path, contents):
3724 with open(path, 'w') as f:
3725 json.dump(contents, f)
3726
3727
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003728@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003730 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003731
3732 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003733 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003734 parser.add_option('-r', '--reverse', action='store_true',
3735 help='Lookup the branch(es) for the specified issues. If '
3736 'no issues are specified, all branches with mapped '
3737 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003738 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003739 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003740 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003741 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003742
dnj@chromium.org406c4402015-03-03 17:22:28 +00003743 if options.reverse:
3744 branches = RunGit(['for-each-ref', 'refs/heads',
3745 '--format=%(refname:short)']).splitlines()
3746
3747 # Reverse issue lookup.
3748 issue_branch_map = {}
3749 for branch in branches:
3750 cl = Changelist(branchref=branch)
3751 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3752 if not args:
3753 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003754 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003755 for issue in args:
3756 if not issue:
3757 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003758 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003759 print('Branch for issue number %s: %s' % (
3760 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003761 if options.json:
3762 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003763 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003764 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003765 if len(args) > 0:
3766 try:
3767 issue = int(args[0])
3768 except ValueError:
3769 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003770 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003771 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003773 if options.json:
3774 write_json(options.json, {
3775 'issue': cl.GetIssue(),
3776 'issue_url': cl.GetIssueURL(),
3777 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003778 return 0
3779
3780
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003781def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003782 """Shows or posts review comments for any changelist."""
3783 parser.add_option('-a', '--add-comment', dest='comment',
3784 help='comment to add to an issue')
3785 parser.add_option('-i', dest='issue',
3786 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003787 parser.add_option('-j', '--json-file',
3788 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003789 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003790 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003791 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003792
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003793 issue = None
3794 if options.issue:
3795 try:
3796 issue = int(options.issue)
3797 except ValueError:
3798 DieWithError('A review issue id is expected to be a number')
3799
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003800 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003801
3802 if options.comment:
3803 cl.AddComment(options.comment)
3804 return 0
3805
3806 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003807 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003808 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003809 summary.append({
3810 'date': message['date'],
3811 'lgtm': False,
3812 'message': message['text'],
3813 'not_lgtm': False,
3814 'sender': message['sender'],
3815 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003816 if message['disapproval']:
3817 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003818 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003819 elif message['approval']:
3820 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003821 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003822 elif message['sender'] == data['owner_email']:
3823 color = Fore.MAGENTA
3824 else:
3825 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003827 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003828 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003829 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003830 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003831 if options.json_file:
3832 with open(options.json_file, 'wb') as f:
3833 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003834 return 0
3835
3836
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003837@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003838def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003839 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003840 parser.add_option('-d', '--display', action='store_true',
3841 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003842 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003843 help='New description to set for this issue (- for stdin, '
3844 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003845 parser.add_option('-f', '--force', action='store_true',
3846 help='Delete any unpublished Gerrit edits for this issue '
3847 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003848
3849 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003850 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003851 options, args = parser.parse_args(args)
3852 _process_codereview_select_options(parser, options)
3853
3854 target_issue = None
3855 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003856 target_issue = ParseIssueNumberArgument(args[0])
3857 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003858 parser.print_help()
3859 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003860
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003861 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003862
martiniss6eda05f2016-06-30 10:18:35 -07003863 kwargs = {
3864 'auth_config': auth_config,
3865 'codereview': options.forced_codereview,
3866 }
3867 if target_issue:
3868 kwargs['issue'] = target_issue.issue
3869 if options.forced_codereview == 'rietveld':
3870 kwargs['rietveld_server'] = target_issue.hostname
3871
3872 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003873
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003874 if not cl.GetIssue():
3875 DieWithError('This branch has no associated changelist.')
3876 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003877
smut@google.com34fb6b12015-07-13 20:03:26 +00003878 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003879 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003880 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003881
3882 if options.new_description:
3883 text = options.new_description
3884 if text == '-':
3885 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003886 elif text == '+':
3887 base_branch = cl.GetCommonAncestorWithUpstream()
3888 change = cl.GetChange(base_branch, None, local_description=True)
3889 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003890
3891 description.set_description(text)
3892 else:
3893 description.prompt()
3894
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003895 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003896 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003897 return 0
3898
3899
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900def CreateDescriptionFromLog(args):
3901 """Pulls out the commit log to use as a base for the CL description."""
3902 log_args = []
3903 if len(args) == 1 and not args[0].endswith('.'):
3904 log_args = [args[0] + '..']
3905 elif len(args) == 1 and args[0].endswith('...'):
3906 log_args = [args[0][:-1]]
3907 elif len(args) == 2:
3908 log_args = [args[0] + '..' + args[1]]
3909 else:
3910 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003911 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912
3913
thestig@chromium.org44202a22014-03-11 19:22:18 +00003914def CMDlint(parser, args):
3915 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003916 parser.add_option('--filter', action='append', metavar='-x,+y',
3917 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003918 auth.add_auth_options(parser)
3919 options, args = parser.parse_args(args)
3920 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003921
3922 # Access to a protected member _XX of a client class
3923 # pylint: disable=W0212
3924 try:
3925 import cpplint
3926 import cpplint_chromium
3927 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003928 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003929 return 1
3930
3931 # Change the current working directory before calling lint so that it
3932 # shows the correct base.
3933 previous_cwd = os.getcwd()
3934 os.chdir(settings.GetRoot())
3935 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003937 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3938 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003939 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003940 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003941 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003942
3943 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003944 command = args + files
3945 if options.filter:
3946 command = ['--filter=' + ','.join(options.filter)] + command
3947 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003948
3949 white_regex = re.compile(settings.GetLintRegex())
3950 black_regex = re.compile(settings.GetLintIgnoreRegex())
3951 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3952 for filename in filenames:
3953 if white_regex.match(filename):
3954 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003955 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003956 else:
3957 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3958 extra_check_functions)
3959 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003960 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003961 finally:
3962 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003963 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003964 if cpplint._cpplint_state.error_count != 0:
3965 return 1
3966 return 0
3967
3968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003969def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003970 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003971 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003972 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003973 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003974 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003975 auth.add_auth_options(parser)
3976 options, args = parser.parse_args(args)
3977 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978
sbc@chromium.org71437c02015-04-09 19:29:40 +00003979 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003980 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981 return 1
3982
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003983 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 if args:
3985 base_branch = args[0]
3986 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003987 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003988 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003990 cl.RunHook(
3991 committing=not options.upload,
3992 may_prompt=False,
3993 verbose=options.verbose,
3994 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003995 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996
3997
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003998def GenerateGerritChangeId(message):
3999 """Returns Ixxxxxx...xxx change id.
4000
4001 Works the same way as
4002 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4003 but can be called on demand on all platforms.
4004
4005 The basic idea is to generate git hash of a state of the tree, original commit
4006 message, author/committer info and timestamps.
4007 """
4008 lines = []
4009 tree_hash = RunGitSilent(['write-tree'])
4010 lines.append('tree %s' % tree_hash.strip())
4011 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4012 if code == 0:
4013 lines.append('parent %s' % parent.strip())
4014 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4015 lines.append('author %s' % author.strip())
4016 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4017 lines.append('committer %s' % committer.strip())
4018 lines.append('')
4019 # Note: Gerrit's commit-hook actually cleans message of some lines and
4020 # whitespace. This code is not doing this, but it clearly won't decrease
4021 # entropy.
4022 lines.append(message)
4023 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4024 stdin='\n'.join(lines))
4025 return 'I%s' % change_hash.strip()
4026
4027
wittman@chromium.org455dc922015-01-26 20:15:50 +00004028def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4029 """Computes the remote branch ref to use for the CL.
4030
4031 Args:
4032 remote (str): The git remote for the CL.
4033 remote_branch (str): The git remote branch for the CL.
4034 target_branch (str): The target branch specified by the user.
4035 pending_prefix (str): The pending prefix from the settings.
4036 """
4037 if not (remote and remote_branch):
4038 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004039
wittman@chromium.org455dc922015-01-26 20:15:50 +00004040 if target_branch:
4041 # Cannonicalize branch references to the equivalent local full symbolic
4042 # refs, which are then translated into the remote full symbolic refs
4043 # below.
4044 if '/' not in target_branch:
4045 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4046 else:
4047 prefix_replacements = (
4048 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4049 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4050 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4051 )
4052 match = None
4053 for regex, replacement in prefix_replacements:
4054 match = re.search(regex, target_branch)
4055 if match:
4056 remote_branch = target_branch.replace(match.group(0), replacement)
4057 break
4058 if not match:
4059 # This is a branch path but not one we recognize; use as-is.
4060 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004061 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4062 # Handle the refs that need to land in different refs.
4063 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004064
wittman@chromium.org455dc922015-01-26 20:15:50 +00004065 # Create the true path to the remote branch.
4066 # Does the following translation:
4067 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4068 # * refs/remotes/origin/master -> refs/heads/master
4069 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4070 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4071 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4072 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4073 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4074 'refs/heads/')
4075 elif remote_branch.startswith('refs/remotes/branch-heads'):
4076 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4077 # If a pending prefix exists then replace refs/ with it.
4078 if pending_prefix:
4079 remote_branch = remote_branch.replace('refs/', pending_prefix)
4080 return remote_branch
4081
4082
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004083def cleanup_list(l):
4084 """Fixes a list so that comma separated items are put as individual items.
4085
4086 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4087 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4088 """
4089 items = sum((i.split(',') for i in l), [])
4090 stripped_items = (i.strip() for i in items)
4091 return sorted(filter(None, stripped_items))
4092
4093
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004094@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004095def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004096 """Uploads the current changelist to codereview.
4097
4098 Can skip dependency patchset uploads for a branch by running:
4099 git config branch.branch_name.skip-deps-uploads True
4100 To unset run:
4101 git config --unset branch.branch_name.skip-deps-uploads
4102 Can also set the above globally by using the --global flag.
4103 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004104 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4105 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004106 parser.add_option('--bypass-watchlists', action='store_true',
4107 dest='bypass_watchlists',
4108 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004109 parser.add_option('-f', action='store_true', dest='force',
4110 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004111 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004112 parser.add_option('-b', '--bug',
4113 help='pre-populate the bug number(s) for this issue. '
4114 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004115 parser.add_option('--message-file', dest='message_file',
4116 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004117 parser.add_option('-t', dest='title',
4118 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004119 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004120 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004121 help='reviewer email addresses')
4122 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004123 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004124 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004125 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004126 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004127 parser.add_option('--emulate_svn_auto_props',
4128 '--emulate-svn-auto-props',
4129 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004130 dest="emulate_svn_auto_props",
4131 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004132 parser.add_option('-c', '--use-commit-queue', action='store_true',
4133 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004134 parser.add_option('--private', action='store_true',
4135 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004136 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004137 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004138 metavar='TARGET',
4139 help='Apply CL to remote ref TARGET. ' +
4140 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004141 parser.add_option('--squash', action='store_true',
4142 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004143 parser.add_option('--no-squash', action='store_true',
4144 help='Don\'t squash multiple commits into one ' +
4145 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004146 parser.add_option('--topic', default=None,
4147 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004148 parser.add_option('--email', default=None,
4149 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004150 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4151 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004152 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4153 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004154 help='Send the patchset to do a CQ dry run right after '
4155 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004156 parser.add_option('--dependencies', action='store_true',
4157 help='Uploads CLs of all the local branches that depend on '
4158 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004159
rmistry@google.com2dd99862015-06-22 12:22:18 +00004160 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004161 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004162 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004163 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004164 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004165 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004166 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004167
sbc@chromium.org71437c02015-04-09 19:29:40 +00004168 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004169 return 1
4170
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004171 options.reviewers = cleanup_list(options.reviewers)
4172 options.cc = cleanup_list(options.cc)
4173
tandriib80458a2016-06-23 12:20:07 -07004174 if options.message_file:
4175 if options.message:
4176 parser.error('only one of --message and --message-file allowed.')
4177 options.message = gclient_utils.FileRead(options.message_file)
4178 options.message_file = None
4179
tandrii4d0545a2016-07-06 03:56:49 -07004180 if options.cq_dry_run and options.use_commit_queue:
4181 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4182
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004183 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4184 settings.GetIsGerrit()
4185
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004186 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004187 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004188
4189
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004190def IsSubmoduleMergeCommit(ref):
4191 # When submodules are added to the repo, we expect there to be a single
4192 # non-git-svn merge commit at remote HEAD with a signature comment.
4193 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004194 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004195 return RunGit(cmd) != ''
4196
4197
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004199 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004201 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4202 upstream and closes the issue automatically and atomically.
4203
4204 Otherwise (in case of Rietveld):
4205 Squashes branch into a single commit.
4206 Updates changelog with metadata (e.g. pointer to review).
4207 Pushes/dcommits the code upstream.
4208 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209 """
4210 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4211 help='bypass upload presubmit hook')
4212 parser.add_option('-m', dest='message',
4213 help="override review description")
4214 parser.add_option('-f', action='store_true', dest='force',
4215 help="force yes to questions (don't prompt)")
4216 parser.add_option('-c', dest='contributor',
4217 help="external contributor for patch (appended to " +
4218 "description and used as author for git). Should be " +
4219 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004220 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004221 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004223 auth_config = auth.extract_auth_config_from_options(options)
4224
4225 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004227 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4228 if cl.IsGerrit():
4229 if options.message:
4230 # This could be implemented, but it requires sending a new patch to
4231 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4232 # Besides, Gerrit has the ability to change the commit message on submit
4233 # automatically, thus there is no need to support this option (so far?).
4234 parser.error('-m MESSAGE option is not supported for Gerrit.')
4235 if options.contributor:
4236 parser.error(
4237 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4238 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4239 'the contributor\'s "name <email>". If you can\'t upload such a '
4240 'commit for review, contact your repository admin and request'
4241 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004242 if not cl.GetIssue():
4243 DieWithError('You must upload the issue first to Gerrit.\n'
4244 ' If you would rather have `git cl land` upload '
4245 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004246 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4247 options.verbose)
4248
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004249 current = cl.GetBranch()
4250 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4251 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004252 print()
4253 print('Attempting to push branch %r into another local branch!' % current)
4254 print()
4255 print('Either reparent this branch on top of origin/master:')
4256 print(' git reparent-branch --root')
4257 print()
4258 print('OR run `git rebase-update` if you think the parent branch is ')
4259 print('already committed.')
4260 print()
4261 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004262 return 1
4263
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004264 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265 # Default to merging against our best guess of the upstream branch.
4266 args = [cl.GetUpstreamBranch()]
4267
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004268 if options.contributor:
4269 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004270 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004271 return 1
4272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004274 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275
sbc@chromium.org71437c02015-04-09 19:29:40 +00004276 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004277 return 1
4278
4279 # This rev-list syntax means "show all commits not in my branch that
4280 # are in base_branch".
4281 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4282 base_branch]).splitlines()
4283 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004284 print('Base branch "%s" has %d commits '
4285 'not in this branch.' % (base_branch, len(upstream_commits)))
4286 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287 return 1
4288
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004289 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004290 svn_head = None
4291 if cmd == 'dcommit' or base_has_submodules:
4292 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4293 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004296 # If the base_head is a submodule merge commit, the first parent of the
4297 # base_head should be a git-svn commit, which is what we're interested in.
4298 base_svn_head = base_branch
4299 if base_has_submodules:
4300 base_svn_head += '^1'
4301
4302 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('This branch has %d additional commits not upstreamed yet.'
4305 % len(extra_commits.splitlines()))
4306 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4307 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308 return 1
4309
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004310 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004311 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004312 author = None
4313 if options.contributor:
4314 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004315 hook_results = cl.RunHook(
4316 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004317 may_prompt=not options.force,
4318 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004319 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004320 if not hook_results.should_continue():
4321 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004322
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004323 # Check the tree status if the tree status URL is set.
4324 status = GetTreeStatus()
4325 if 'closed' == status:
4326 print('The tree is closed. Please wait for it to reopen. Use '
4327 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4328 return 1
4329 elif 'unknown' == status:
4330 print('Unable to determine tree status. Please verify manually and '
4331 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4332 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004333
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004334 change_desc = ChangeDescription(options.message)
4335 if not change_desc.description and cl.GetIssue():
4336 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004338 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004339 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004340 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004341 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004342 print('No description set.')
4343 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004344 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004346 # Keep a separate copy for the commit message, because the commit message
4347 # contains the link to the Rietveld issue, while the Rietveld message contains
4348 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004349 # Keep a separate copy for the commit message.
4350 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004351 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004352
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004353 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004354 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004355 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004356 # after it. Add a period on a new line to circumvent this. Also add a space
4357 # before the period to make sure that Gitiles continues to correctly resolve
4358 # the URL.
4359 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004361 commit_desc.append_footer('Patch from %s.' % options.contributor)
4362
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004363 print('Description:')
4364 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004365
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004366 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004367 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004368 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004370 # We want to squash all this branch's commits into one commit with the proper
4371 # description. We do this by doing a "reset --soft" to the base branch (which
4372 # keeps the working copy the same), then dcommitting that. If origin/master
4373 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4374 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004375 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004376 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4377 # Delete the branches if they exist.
4378 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4379 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4380 result = RunGitWithCode(showref_cmd)
4381 if result[0] == 0:
4382 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004383
4384 # We might be in a directory that's present in this branch but not in the
4385 # trunk. Move up to the top of the tree so that git commands that expect a
4386 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004387 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388 if rel_base_path:
4389 os.chdir(rel_base_path)
4390
4391 # Stuff our change into the merge branch.
4392 # We wrap in a try...finally block so if anything goes wrong,
4393 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004394 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004395 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004396 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004397 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004399 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004400 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004402 RunGit(
4403 [
4404 'commit', '--author', options.contributor,
4405 '-m', commit_desc.description,
4406 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004408 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004409 if base_has_submodules:
4410 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4411 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4412 RunGit(['checkout', CHERRY_PICK_BRANCH])
4413 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004414 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004415 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004416 mirror = settings.GetGitMirror(remote)
4417 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004418 pending_prefix = settings.GetPendingRefPrefix()
4419 if not pending_prefix or branch.startswith(pending_prefix):
4420 # If not using refs/pending/heads/* at all, or target ref is already set
4421 # to pending, then push to the target ref directly.
4422 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004423 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004424 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004425 else:
4426 # Cherry-pick the change on top of pending ref and then push it.
4427 assert branch.startswith('refs/'), branch
4428 assert pending_prefix[-1] == '/', pending_prefix
4429 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004430 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004431 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004432 if retcode == 0:
4433 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004434 else:
4435 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004436 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004437 'svn', 'dcommit',
4438 '-C%s' % options.similarity,
4439 '--no-rebase', '--rmdir',
4440 ]
4441 if settings.GetForceHttpsCommitUrl():
4442 # Allow forcing https commit URLs for some projects that don't allow
4443 # committing to http URLs (like Google Code).
4444 remote_url = cl.GetGitSvnRemoteUrl()
4445 if urlparse.urlparse(remote_url).scheme == 'http':
4446 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004447 cmd_args.append('--commit-url=%s' % remote_url)
4448 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004449 if 'Committed r' in output:
4450 revision = re.match(
4451 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4452 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004453 finally:
4454 # And then swap back to the original branch and clean up.
4455 RunGit(['checkout', '-q', cl.GetBranch()])
4456 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004457 if base_has_submodules:
4458 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004459
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004460 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004462 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004463
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004464 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004465 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004466 try:
4467 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4468 # We set pushed_to_pending to False, since it made it all the way to the
4469 # real ref.
4470 pushed_to_pending = False
4471 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004472 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004474 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004475 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004476 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004477 if not to_pending:
4478 if viewvc_url and revision:
4479 change_desc.append_footer(
4480 'Committed: %s%s' % (viewvc_url, revision))
4481 elif revision:
4482 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004483 print('Closing issue '
4484 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004485 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004486 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004487 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004488 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004489 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004490 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004491 if options.bypass_hooks:
4492 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4493 else:
4494 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004495 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004496
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004497 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004498 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('The commit is in the pending queue (%s).' % pending_ref)
4500 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4501 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004502
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004503 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4504 if os.path.isfile(hook):
4505 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004506
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004507 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004508
4509
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004510def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004511 print()
4512 print('Waiting for commit to be landed on %s...' % real_ref)
4513 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004514 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4515 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004516 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004517
4518 loop = 0
4519 while True:
4520 sys.stdout.write('fetching (%d)... \r' % loop)
4521 sys.stdout.flush()
4522 loop += 1
4523
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004524 if mirror:
4525 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004526 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4527 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4528 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4529 for commit in commits.splitlines():
4530 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004531 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004532 return commit
4533
4534 current_rev = to_rev
4535
4536
tandriibf429402016-09-14 07:09:12 -07004537def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004538 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4539
4540 Returns:
4541 (retcode of last operation, output log of last operation).
4542 """
4543 assert pending_ref.startswith('refs/'), pending_ref
4544 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4545 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4546 code = 0
4547 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004548 max_attempts = 3
4549 attempts_left = max_attempts
4550 while attempts_left:
4551 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004552 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004553 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004554
4555 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004556 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004557 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004558 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004559 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004560 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004561 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004562 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004563 continue
4564
4565 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004566 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004567 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004568 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004569 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004570 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4571 'the following files have merge conflicts:' % pending_ref)
4572 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4573 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004574 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004575 return code, out
4576
4577 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004578 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004579 code, out = RunGitWithCode(
4580 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4581 if code == 0:
4582 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004583 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004584 return code, out
4585
vapiera7fbd5a2016-06-16 09:17:49 -07004586 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004587 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004588 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004589 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004590 print('Fatal push error. Make sure your .netrc credentials and git '
4591 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004592 return code, out
4593
vapiera7fbd5a2016-06-16 09:17:49 -07004594 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004595 return code, out
4596
4597
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004598def IsFatalPushFailure(push_stdout):
4599 """True if retrying push won't help."""
4600 return '(prohibited by Gerrit)' in push_stdout
4601
4602
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004603@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004605 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004607 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004608 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004609 message = """This repository appears to be a git-svn mirror, but we
4610don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004611 else:
4612 message = """This doesn't appear to be an SVN repository.
4613If your project has a true, writeable git repository, you probably want to run
4614'git cl land' instead.
4615If your project has a git mirror of an upstream SVN master, you probably need
4616to run 'git svn init'.
4617
4618Using the wrong command might cause your commit to appear to succeed, and the
4619review to be closed, without actually landing upstream. If you choose to
4620proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004621 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004622 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004623 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4624 'Please let us know of this project you are committing to:'
4625 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626 return SendUpstream(parser, args, 'dcommit')
4627
4628
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004629@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004630def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004631 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004632 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633 print('This appears to be an SVN repository.')
4634 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004635 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004636 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004637 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004638
4639
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004640@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004641def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004642 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 parser.add_option('-b', dest='newbranch',
4644 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004645 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004647 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4648 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004649 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004650 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004651 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004652 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004654 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004655
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004656
4657 group = optparse.OptionGroup(
4658 parser,
4659 'Options for continuing work on the current issue uploaded from a '
4660 'different clone (e.g. different machine). Must be used independently '
4661 'from the other options. No issue number should be specified, and the '
4662 'branch must have an issue number associated with it')
4663 group.add_option('--reapply', action='store_true', dest='reapply',
4664 help='Reset the branch and reapply the issue.\n'
4665 'CAUTION: This will undo any local changes in this '
4666 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004667
4668 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004669 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004670 parser.add_option_group(group)
4671
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004672 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004673 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004674 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004675 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004676 auth_config = auth.extract_auth_config_from_options(options)
4677
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004678
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004679 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004680 if options.newbranch:
4681 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004682 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004683 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004684
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004685 cl = Changelist(auth_config=auth_config,
4686 codereview=options.forced_codereview)
4687 if not cl.GetIssue():
4688 parser.error('current branch must have an associated issue')
4689
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004690 upstream = cl.GetUpstreamBranch()
4691 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004692 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004693
4694 RunGit(['reset', '--hard', upstream])
4695 if options.pull:
4696 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004697
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004698 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4699 options.directory)
4700
4701 if len(args) != 1 or not args[0]:
4702 parser.error('Must specify issue number or url')
4703
4704 # We don't want uncommitted changes mixed up with the patch.
4705 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004706 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004707
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004708 if options.newbranch:
4709 if options.force:
4710 RunGit(['branch', '-D', options.newbranch],
4711 stderr=subprocess2.PIPE, error_ok=True)
4712 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004713 elif not GetCurrentBranch():
4714 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004715
4716 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4717
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004718 if cl.IsGerrit():
4719 if options.reject:
4720 parser.error('--reject is not supported with Gerrit codereview.')
4721 if options.nocommit:
4722 parser.error('--nocommit is not supported with Gerrit codereview.')
4723 if options.directory:
4724 parser.error('--directory is not supported with Gerrit codereview.')
4725
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004726 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004727 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004728
4729
4730def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004731 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004732 # Provide a wrapper for git svn rebase to help avoid accidental
4733 # git svn dcommit.
4734 # It's the only command that doesn't use parser at all since we just defer
4735 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004736
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004737 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004738
4739
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004740def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741 """Fetches the tree status and returns either 'open', 'closed',
4742 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004743 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004744 if url:
4745 status = urllib2.urlopen(url).read().lower()
4746 if status.find('closed') != -1 or status == '0':
4747 return 'closed'
4748 elif status.find('open') != -1 or status == '1':
4749 return 'open'
4750 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004751 return 'unset'
4752
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004753
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004754def GetTreeStatusReason():
4755 """Fetches the tree status from a json url and returns the message
4756 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004757 url = settings.GetTreeStatusUrl()
4758 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004759 connection = urllib2.urlopen(json_url)
4760 status = json.loads(connection.read())
4761 connection.close()
4762 return status['message']
4763
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004765def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004766 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004767 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004768 status = GetTreeStatus()
4769 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004770 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004771 return 2
4772
vapiera7fbd5a2016-06-16 09:17:49 -07004773 print('The tree is %s' % status)
4774 print()
4775 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776 if status != 'open':
4777 return 1
4778 return 0
4779
4780
maruel@chromium.org15192402012-09-06 12:38:29 +00004781def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004782 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004783 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004784 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004785 '-b', '--bot', action='append',
4786 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4787 'times to specify multiple builders. ex: '
4788 '"-b win_rel -b win_layout". See '
4789 'the try server waterfall for the builders name and the tests '
4790 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004791 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004792 '-B', '--bucket', default='',
4793 help=('Buildbucket bucket to send the try requests.'))
4794 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004795 '-m', '--master', default='',
4796 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004797 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004798 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004799 help='Revision to use for the try job; default: the revision will '
4800 'be determined by the try recipe that builder runs, which usually '
4801 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004802 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004803 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004804 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004805 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004806 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004807 '--project',
4808 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004809 'in recipe to determine to which repository or directory to '
4810 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004811 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004812 '-p', '--property', dest='properties', action='append', default=[],
4813 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004814 'key2=value2 etc. The value will be treated as '
4815 'json if decodable, or as string otherwise. '
4816 'NOTE: using this may make your try job not usable for CQ, '
4817 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004818 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004819 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4820 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004821 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004822 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004823 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004824 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004825
machenbach@chromium.org45453142015-09-15 08:45:22 +00004826 # Make sure that all properties are prop=value pairs.
4827 bad_params = [x for x in options.properties if '=' not in x]
4828 if bad_params:
4829 parser.error('Got properties with missing "=": %s' % bad_params)
4830
maruel@chromium.org15192402012-09-06 12:38:29 +00004831 if args:
4832 parser.error('Unknown arguments: %s' % args)
4833
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004834 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004835 if not cl.GetIssue():
4836 parser.error('Need to upload first')
4837
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004838 if cl.IsGerrit():
4839 parser.error(
4840 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4841 'If your project has Commit Queue, dry run is a workaround:\n'
4842 ' git cl set-commit --dry-run')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004843
tandriie113dfd2016-10-11 10:20:12 -07004844 error_message = cl.CannotTriggerTryJobReason()
4845 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004846 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004847
borenet6c0efe62016-10-19 08:13:29 -07004848 if options.bucket and options.master:
4849 parser.error('Only one of --bucket and --master may be used.')
4850
qyearsley1fdfcb62016-10-24 13:22:03 -07004851 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004852
qyearsleydd49f942016-10-28 11:57:22 -07004853 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4854 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004855 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004856 if options.verbose:
4857 print('git cl try with no bots now defaults to CQ Dry Run.')
4858 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004859
borenet6c0efe62016-10-19 08:13:29 -07004860 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004861 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004862 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004863 'of bot requires an initial job from a parent (usually a builder). '
4864 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004865 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004866 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004867
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004868 patchset = cl.GetMostRecentPatchset()
tandriide281ae2016-10-12 06:02:30 -07004869 if patchset != cl.GetPatchset():
4870 print('Warning: Codereview server has newer patchsets (%s) than most '
4871 'recent upload from local checkout (%s). Did a previous upload '
4872 'fail?\n'
4873 'By default, git cl try uses the latest patchset from '
4874 'codereview, continuing to use patchset %s.\n' %
4875 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004876
tandrii568043b2016-10-11 07:49:18 -07004877 try:
borenet6c0efe62016-10-19 08:13:29 -07004878 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4879 patchset)
tandrii568043b2016-10-11 07:49:18 -07004880 except BuildbucketResponseException as ex:
4881 print('ERROR: %s' % ex)
4882 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004883 return 0
4884
4885
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004886def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004887 """Prints info about try jobs associated with current CL."""
4888 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004889 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004890 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004891 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004892 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004893 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004894 '--color', action='store_true', default=setup_color.IS_TTY,
4895 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004896 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004897 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4898 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004899 group.add_option(
4900 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004901 parser.add_option_group(group)
4902 auth.add_auth_options(parser)
4903 options, args = parser.parse_args(args)
4904 if args:
4905 parser.error('Unrecognized args: %s' % ' '.join(args))
4906
4907 auth_config = auth.extract_auth_config_from_options(options)
4908 cl = Changelist(auth_config=auth_config)
4909 if not cl.GetIssue():
4910 parser.error('Need to upload first')
4911
tandrii221ab252016-10-06 08:12:04 -07004912 patchset = options.patchset
4913 if not patchset:
4914 patchset = cl.GetMostRecentPatchset()
4915 if not patchset:
4916 parser.error('Codereview doesn\'t know about issue %s. '
4917 'No access to issue or wrong issue number?\n'
4918 'Either upload first, or pass --patchset explicitely' %
4919 cl.GetIssue())
4920
4921 if patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004922 print('Warning: Codereview server has newer patchsets (%s) than most '
4923 'recent upload from local checkout (%s). Did a previous upload '
4924 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004925 'By default, git cl try-results uses the latest patchset from '
4926 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004927 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004928 try:
tandrii221ab252016-10-06 08:12:04 -07004929 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004931 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004932 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004933 if options.json:
4934 write_try_results_json(options.json, jobs)
4935 else:
4936 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004937 return 0
4938
4939
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004940@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004941def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004942 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004943 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004944 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004945 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004947 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004948 if args:
4949 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004950 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004951 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004952 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004953 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004954
4955 # Clear configured merge-base, if there is one.
4956 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004957 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004958 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004959 return 0
4960
4961
thestig@chromium.org00858c82013-12-02 23:08:03 +00004962def CMDweb(parser, args):
4963 """Opens the current CL in the web browser."""
4964 _, args = parser.parse_args(args)
4965 if args:
4966 parser.error('Unrecognized args: %s' % ' '.join(args))
4967
4968 issue_url = Changelist().GetIssueURL()
4969 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004970 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004971 return 1
4972
4973 webbrowser.open(issue_url)
4974 return 0
4975
4976
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004977def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004978 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004979 parser.add_option('-d', '--dry-run', action='store_true',
4980 help='trigger in dry run mode')
4981 parser.add_option('-c', '--clear', action='store_true',
4982 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004983 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004984 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004985 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004986 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004987 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004988 if args:
4989 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004990 if options.dry_run and options.clear:
4991 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4992
iannuccie53c9352016-08-17 14:40:40 -07004993 cl = Changelist(auth_config=auth_config, issue=options.issue,
4994 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004995 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004996 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004997 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004998 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004999 state = _CQState.DRY_RUN
5000 else:
5001 state = _CQState.COMMIT
5002 if not cl.GetIssue():
5003 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005004 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005005 return 0
5006
5007
groby@chromium.org411034a2013-02-26 15:12:01 +00005008def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005009 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005010 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005011 auth.add_auth_options(parser)
5012 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005013 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005014 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005015 if args:
5016 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005017 cl = Changelist(auth_config=auth_config, issue=options.issue,
5018 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005019 # Ensure there actually is an issue to close.
5020 cl.GetDescription()
5021 cl.CloseIssue()
5022 return 0
5023
5024
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005025def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005026 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005027 parser.add_option(
5028 '--stat',
5029 action='store_true',
5030 dest='stat',
5031 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005032 auth.add_auth_options(parser)
5033 options, args = parser.parse_args(args)
5034 auth_config = auth.extract_auth_config_from_options(options)
5035 if args:
5036 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005037
5038 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005039 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005040 # Staged changes would be committed along with the patch from last
5041 # upload, hence counted toward the "last upload" side in the final
5042 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005043 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005044 return 1
5045
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005046 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005047 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005048 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005049 if not issue:
5050 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005051 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005052 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005053
5054 # Create a new branch based on the merge-base
5055 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005056 # Clear cached branch in cl object, to avoid overwriting original CL branch
5057 # properties.
5058 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005059 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005060 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005061 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005062 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005063 return rtn
5064
wychen@chromium.org06928532015-02-03 02:11:29 +00005065 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005066 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005067 cmd = ['git', 'diff']
5068 if options.stat:
5069 cmd.append('--stat')
5070 cmd.extend([TMP_BRANCH, branch, '--'])
5071 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005072 finally:
5073 RunGit(['checkout', '-q', branch])
5074 RunGit(['branch', '-D', TMP_BRANCH])
5075
5076 return 0
5077
5078
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005079def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005080 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005081 parser.add_option(
5082 '--no-color',
5083 action='store_true',
5084 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005085 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005086 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005087 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005088
5089 author = RunGit(['config', 'user.email']).strip() or None
5090
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005091 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005092
5093 if args:
5094 if len(args) > 1:
5095 parser.error('Unknown args')
5096 base_branch = args[0]
5097 else:
5098 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005099 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005100
5101 change = cl.GetChange(base_branch, None)
5102 return owners_finder.OwnersFinder(
5103 [f.LocalPath() for f in
5104 cl.GetChange(base_branch, None).AffectedFiles()],
5105 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005106 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005107 disable_color=options.no_color).run()
5108
5109
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005110def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005111 """Generates a diff command."""
5112 # Generate diff for the current branch's changes.
5113 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5114 upstream_commit, '--' ]
5115
5116 if args:
5117 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005118 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005119 diff_cmd.append(arg)
5120 else:
5121 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005122
5123 return diff_cmd
5124
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005125def MatchingFileType(file_name, extensions):
5126 """Returns true if the file name ends with one of the given extensions."""
5127 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005128
enne@chromium.org555cfe42014-01-29 18:21:39 +00005129@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005130def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005131 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005132 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005133 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005134 parser.add_option('--full', action='store_true',
5135 help='Reformat the full content of all touched files')
5136 parser.add_option('--dry-run', action='store_true',
5137 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005138 parser.add_option('--python', action='store_true',
5139 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005140 parser.add_option('--diff', action='store_true',
5141 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005142 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005143
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005144 # git diff generates paths against the root of the repository. Change
5145 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005146 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005147 if rel_base_path:
5148 os.chdir(rel_base_path)
5149
digit@chromium.org29e47272013-05-17 17:01:46 +00005150 # Grab the merge-base commit, i.e. the upstream commit of the current
5151 # branch when it was created or the last time it was rebased. This is
5152 # to cover the case where the user may have called "git fetch origin",
5153 # moving the origin branch to a newer commit, but hasn't rebased yet.
5154 upstream_commit = None
5155 cl = Changelist()
5156 upstream_branch = cl.GetUpstreamBranch()
5157 if upstream_branch:
5158 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5159 upstream_commit = upstream_commit.strip()
5160
5161 if not upstream_commit:
5162 DieWithError('Could not find base commit for this branch. '
5163 'Are you in detached state?')
5164
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005165 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5166 diff_output = RunGit(changed_files_cmd)
5167 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005168 # Filter out files deleted by this CL
5169 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005170
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005171 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5172 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5173 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005174 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005175
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005176 top_dir = os.path.normpath(
5177 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5178
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005179 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5180 # formatted. This is used to block during the presubmit.
5181 return_value = 0
5182
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005183 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005184 # Locate the clang-format binary in the checkout
5185 try:
5186 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005187 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005188 DieWithError(e)
5189
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005190 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005191 cmd = [clang_format_tool]
5192 if not opts.dry_run and not opts.diff:
5193 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005194 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005195 if opts.diff:
5196 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005197 else:
5198 env = os.environ.copy()
5199 env['PATH'] = str(os.path.dirname(clang_format_tool))
5200 try:
5201 script = clang_format.FindClangFormatScriptInChromiumTree(
5202 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005203 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005204 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005205
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005206 cmd = [sys.executable, script, '-p0']
5207 if not opts.dry_run and not opts.diff:
5208 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005209
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005210 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5211 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005212
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005213 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5214 if opts.diff:
5215 sys.stdout.write(stdout)
5216 if opts.dry_run and len(stdout) > 0:
5217 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005218
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005219 # Similar code to above, but using yapf on .py files rather than clang-format
5220 # on C/C++ files
5221 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005222 yapf_tool = gclient_utils.FindExecutable('yapf')
5223 if yapf_tool is None:
5224 DieWithError('yapf not found in PATH')
5225
5226 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005227 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005228 cmd = [yapf_tool]
5229 if not opts.dry_run and not opts.diff:
5230 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005231 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005232 if opts.diff:
5233 sys.stdout.write(stdout)
5234 else:
5235 # TODO(sbc): yapf --lines mode still has some issues.
5236 # https://github.com/google/yapf/issues/154
5237 DieWithError('--python currently only works with --full')
5238
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005239 # Dart's formatter does not have the nice property of only operating on
5240 # modified chunks, so hard code full.
5241 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005242 try:
5243 command = [dart_format.FindDartFmtToolInChromiumTree()]
5244 if not opts.dry_run and not opts.diff:
5245 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005246 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005247
ppi@chromium.org6593d932016-03-03 15:41:15 +00005248 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005249 if opts.dry_run and stdout:
5250 return_value = 2
5251 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005252 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5253 'found in this checkout. Files in other languages are still '
5254 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005255
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005256 # Format GN build files. Always run on full build files for canonical form.
5257 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005258 cmd = ['gn', 'format' ]
5259 if opts.dry_run or opts.diff:
5260 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005261 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005262 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5263 shell=sys.platform == 'win32',
5264 cwd=top_dir)
5265 if opts.dry_run and gn_ret == 2:
5266 return_value = 2 # Not formatted.
5267 elif opts.diff and gn_ret == 2:
5268 # TODO this should compute and print the actual diff.
5269 print("This change has GN build file diff for " + gn_diff_file)
5270 elif gn_ret != 0:
5271 # For non-dry run cases (and non-2 return values for dry-run), a
5272 # nonzero error code indicates a failure, probably because the file
5273 # doesn't parse.
5274 DieWithError("gn format failed on " + gn_diff_file +
5275 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005276
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005277 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005278
5279
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005280@subcommand.usage('<codereview url or issue id>')
5281def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005282 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005283 _, args = parser.parse_args(args)
5284
5285 if len(args) != 1:
5286 parser.print_help()
5287 return 1
5288
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005289 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005290 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005291 parser.print_help()
5292 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005293 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005294
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005295 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005296 output = RunGit(['config', '--local', '--get-regexp',
5297 r'branch\..*\.%s' % issueprefix],
5298 error_ok=True)
5299 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005300 if issue == target_issue:
5301 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005302
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005303 branches = []
5304 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005305 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005306 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005307 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005308 return 1
5309 if len(branches) == 1:
5310 RunGit(['checkout', branches[0]])
5311 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005312 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005313 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005314 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005315 which = raw_input('Choose by index: ')
5316 try:
5317 RunGit(['checkout', branches[int(which)]])
5318 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005319 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005320 return 1
5321
5322 return 0
5323
5324
maruel@chromium.org29404b52014-09-08 22:58:00 +00005325def CMDlol(parser, args):
5326 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005327 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005328 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5329 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5330 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005331 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005332 return 0
5333
5334
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005335class OptionParser(optparse.OptionParser):
5336 """Creates the option parse and add --verbose support."""
5337 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005338 optparse.OptionParser.__init__(
5339 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005340 self.add_option(
5341 '-v', '--verbose', action='count', default=0,
5342 help='Use 2 times for more debugging info')
5343
5344 def parse_args(self, args=None, values=None):
5345 options, args = optparse.OptionParser.parse_args(self, args, values)
5346 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5347 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5348 return options, args
5349
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005351def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005352 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005353 print('\nYour python version %s is unsupported, please upgrade.\n' %
5354 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005355 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005356
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005357 # Reload settings.
5358 global settings
5359 settings = Settings()
5360
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005361 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005362 dispatcher = subcommand.CommandDispatcher(__name__)
5363 try:
5364 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005365 except auth.AuthenticationError as e:
5366 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005367 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005368 if e.code != 500:
5369 raise
5370 DieWithError(
5371 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5372 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005373 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005374
5375
5376if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005377 # These affect sys.stdout so do it outside of main() to simplify mocks in
5378 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005379 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005380 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005381 try:
5382 sys.exit(main(sys.argv[1:]))
5383 except KeyboardInterrupt:
5384 sys.stderr.write('interrupted\n')
5385 sys.exit(1)