blob: 3b92b9034298e56b5de3c5f9ef815e4324264856 [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
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010016import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080036 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
skobes6468b902016-10-24 08:45:10 -070044import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
tandrii9d2c7a32016-06-22 03:42:45 -070066COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070067DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080068POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
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
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010088# Used by tests/git_cl_test.py to add extra logging.
89# Inside the weirdly failing test, add this:
90# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
91# And scroll up to see the strack trace printed.
92_IS_BEING_TESTED = False
93
maruel@chromium.orgddd59412011-11-30 14:20:38 +000094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070096 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 sys.exit(1)
98
99
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000100def GetNoGitPagerEnv():
101 env = os.environ.copy()
102 # 'cat' is a magical git string that disables pagers on all platforms.
103 env['GIT_PAGER'] = 'cat'
104 return env
105
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000106
bsep@chromium.org627d9002016-04-29 00:00:52 +0000107def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000109 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000110 except subprocess2.CalledProcessError as e:
111 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 'Command "%s" failed.\n%s' % (
115 ' '.join(args), error_message or e.stdout or ''))
116 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000117
118
119def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000120 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000122
123
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700126 if suppress_stderr:
127 stderr = subprocess2.VOID
128 else:
129 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000130 try:
tandrii5d48c322016-08-18 16:19:37 -0700131 (out, _), code = subprocess2.communicate(['git'] + args,
132 env=GetNoGitPagerEnv(),
133 stdout=subprocess2.PIPE,
134 stderr=stderr)
135 return code, out
136 except subprocess2.CalledProcessError as e:
137 logging.debug('Failed running %s', args)
138 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139
140
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000141def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000142 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000143 return RunGitWithCode(args, suppress_stderr=True)[1]
144
145
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000146def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000147 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000148 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000149 return (version.startswith(prefix) and
150 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000151
152
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000153def BranchExists(branch):
154 """Return True if specified branch exists."""
155 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
156 suppress_stderr=True)
157 return not code
158
159
tandrii2a16b952016-10-19 07:09:44 -0700160def time_sleep(seconds):
161 # Use this so that it can be mocked in tests without interfering with python
162 # system machinery.
163 import time # Local import to discourage others from importing time globally.
164 return time.sleep(seconds)
165
166
maruel@chromium.org90541732011-04-01 17:54:18 +0000167def ask_for_data(prompt):
168 try:
169 return raw_input(prompt)
170 except KeyboardInterrupt:
171 # Hide the exception.
172 sys.exit(1)
173
174
tandrii5d48c322016-08-18 16:19:37 -0700175def _git_branch_config_key(branch, key):
176 """Helper method to return Git config key for a branch."""
177 assert branch, 'branch name is required to set git config for it'
178 return 'branch.%s.%s' % (branch, key)
179
180
181def _git_get_branch_config_value(key, default=None, value_type=str,
182 branch=False):
183 """Returns git config value of given or current branch if any.
184
185 Returns default in all other cases.
186 """
187 assert value_type in (int, str, bool)
188 if branch is False: # Distinguishing default arg value from None.
189 branch = GetCurrentBranch()
190
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000191 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700192 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000193
tandrii5d48c322016-08-18 16:19:37 -0700194 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700195 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700196 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700197 # git config also has --int, but apparently git config suffers from integer
198 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700199 args.append(_git_branch_config_key(branch, key))
200 code, out = RunGitWithCode(args)
201 if code == 0:
202 value = out.strip()
203 if value_type == int:
204 return int(value)
205 if value_type == bool:
206 return bool(value.lower() == 'true')
207 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208 return default
209
210
tandrii5d48c322016-08-18 16:19:37 -0700211def _git_set_branch_config_value(key, value, branch=None, **kwargs):
212 """Sets the value or unsets if it's None of a git branch config.
213
214 Valid, though not necessarily existing, branch must be provided,
215 otherwise currently checked out branch is used.
216 """
217 if not branch:
218 branch = GetCurrentBranch()
219 assert branch, 'a branch name OR currently checked out branch is required'
220 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700221 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700222 if value is None:
223 args.append('--unset')
224 elif isinstance(value, bool):
225 args.append('--bool')
226 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700227 else:
tandrii33a46ff2016-08-23 05:53:40 -0700228 # git config also has --int, but apparently git config suffers from integer
229 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700230 value = str(value)
231 args.append(_git_branch_config_key(branch, key))
232 if value is not None:
233 args.append(value)
234 RunGit(args, **kwargs)
235
236
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100237def _get_committer_timestamp(commit):
238 """Returns unix timestamp as integer of a committer in a commit.
239
240 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
241 """
242 # Git also stores timezone offset, but it only affects visual display,
243 # actual point in time is defined by this timestamp only.
244 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
245
246
247def _git_amend_head(message, committer_timestamp):
248 """Amends commit with new message and desired committer_timestamp.
249
250 Sets committer timezone to UTC.
251 """
252 env = os.environ.copy()
253 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
254 return RunGit(['commit', '--amend', '-m', message], env=env)
255
256
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000257def add_git_similarity(parser):
258 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700259 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000260 help='Sets the percentage that a pair of files need to match in order to'
261 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000262 parser.add_option(
263 '--find-copies', action='store_true',
264 help='Allows git to look for copies.')
265 parser.add_option(
266 '--no-find-copies', action='store_false', dest='find_copies',
267 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000268
269 old_parser_args = parser.parse_args
270 def Parse(args):
271 options, args = old_parser_args(args)
272
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000273 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700274 options.similarity = _git_get_branch_config_value(
275 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000276 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000277 print('Note: Saving similarity of %d%% in git config.'
278 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700279 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000280
iannucci@chromium.org79540052012-10-19 23:15:26 +0000281 options.similarity = max(0, min(options.similarity, 100))
282
283 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700284 options.find_copies = _git_get_branch_config_value(
285 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000286 else:
tandrii5d48c322016-08-18 16:19:37 -0700287 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000288
289 print('Using %d%% similarity for rename/copy detection. '
290 'Override with --similarity.' % options.similarity)
291
292 return options, args
293 parser.parse_args = Parse
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def _get_properties_from_options(options):
297 properties = dict(x.split('=', 1) for x in options.properties)
298 for key, val in properties.iteritems():
299 try:
300 properties[key] = json.loads(val)
301 except ValueError:
302 pass # If a value couldn't be evaluated, treat it as a string.
303 return properties
304
305
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306def _prefix_master(master):
307 """Convert user-specified master name to full master name.
308
309 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
310 name, while the developers always use shortened master name
311 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
312 function does the conversion for buildbucket migration.
313 """
borenet6c0efe62016-10-19 08:13:29 -0700314 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000315 return master
borenet6c0efe62016-10-19 08:13:29 -0700316 return '%s%s' % (MASTER_PREFIX, master)
317
318
319def _unprefix_master(bucket):
320 """Convert bucket name to shortened master name.
321
322 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
323 name, while the developers always use shortened master name
324 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
325 function does the conversion for buildbucket migration.
326 """
327 if bucket.startswith(MASTER_PREFIX):
328 return bucket[len(MASTER_PREFIX):]
329 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330
331
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000332def _buildbucket_retry(operation_name, http, *args, **kwargs):
333 """Retries requests to buildbucket service and returns parsed json content."""
334 try_count = 0
335 while True:
336 response, content = http.request(*args, **kwargs)
337 try:
338 content_json = json.loads(content)
339 except ValueError:
340 content_json = None
341
342 # Buildbucket could return an error even if status==200.
343 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000344 error = content_json.get('error')
345 if error.get('code') == 403:
346 raise BuildbucketResponseException(
347 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000348 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000349 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 raise BuildbucketResponseException(msg)
351
352 if response.status == 200:
353 if not content_json:
354 raise BuildbucketResponseException(
355 'Buildbucket returns invalid json content: %s.\n'
356 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
357 content)
358 return content_json
359 if response.status < 500 or try_count >= 2:
360 raise httplib2.HttpLib2Error(content)
361
362 # status >= 500 means transient failures.
363 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700364 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000365 try_count += 1
366 assert False, 'unreachable'
367
368
qyearsley1fdfcb62016-10-24 13:22:03 -0700369def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700370 """Returns a dict mapping bucket names to builders and tests,
371 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700372 """
qyearsleydd49f942016-10-28 11:57:22 -0700373 # If no bots are listed, we try to get a set of builders and tests based
374 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700375 if not options.bot:
376 change = changelist.GetChange(
377 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700378 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700379 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700380 change=change,
381 changed_files=change.LocalPaths(),
382 repository_root=settings.GetRoot(),
383 default_presubmit=None,
384 project=None,
385 verbose=options.verbose,
386 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700387 if masters is None:
388 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100389 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700390
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 if options.bucket:
392 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700393 if options.master:
394 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700395
qyearsleydd49f942016-10-28 11:57:22 -0700396 # If bots are listed but no master or bucket, then we need to find out
397 # the corresponding master for each bot.
398 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
399 if error_message:
400 option_parser.error(
401 'Tryserver master cannot be found because: %s\n'
402 'Please manually specify the tryserver master, e.g. '
403 '"-m tryserver.chromium.linux".' % error_message)
404 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700405
406
qyearsley123a4682016-10-26 09:12:17 -0700407def _get_bucket_map_for_builders(builders):
408 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 map_url = 'https://builders-map.appspot.com/'
410 try:
qyearsley123a4682016-10-26 09:12:17 -0700411 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700412 except urllib2.URLError as e:
413 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
414 (map_url, e))
415 except ValueError as e:
416 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700417 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 return None, 'Failed to build master map.'
419
qyearsley123a4682016-10-26 09:12:17 -0700420 bucket_map = {}
421 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700422 masters = builders_map.get(builder, [])
423 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700425 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700427 (builder, masters))
428 bucket = _prefix_master(masters[0])
429 bucket_map.setdefault(bucket, {})[builder] = []
430
431 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
433
borenet6c0efe62016-10-19 08:13:29 -0700434def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700435 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700436 """Sends a request to Buildbucket to trigger try jobs for a changelist.
437
438 Args:
439 auth_config: AuthConfig for Rietveld.
440 changelist: Changelist that the try jobs are associated with.
441 buckets: A nested dict mapping bucket names to builders to tests.
442 options: Command-line options.
443 """
tandriide281ae2016-10-12 06:02:30 -0700444 assert changelist.GetIssue(), 'CL must be uploaded first'
445 codereview_url = changelist.GetCodereviewServer()
446 assert codereview_url, 'CL must be uploaded first'
447 patchset = patchset or changelist.GetMostRecentPatchset()
448 assert patchset, 'CL must be uploaded first'
449
450 codereview_host = urlparse.urlparse(codereview_url).hostname
451 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000452 http = authenticator.authorize(httplib2.Http())
453 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700454
455 # TODO(tandrii): consider caching Gerrit CL details just like
456 # _RietveldChangelistImpl does, then caching values in these two variables
457 # won't be necessary.
458 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459
460 buildbucket_put_url = (
461 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000462 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700463 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
464 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
465 hostname=codereview_host,
466 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000467 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700468
469 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
470 shared_parameters_properties['category'] = category
471 if options.clobber:
472 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700473 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700474 if extra_properties:
475 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000476
477 batch_req_body = {'builds': []}
478 print_text = []
479 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700480 for bucket, builders_and_tests in sorted(buckets.iteritems()):
481 print_text.append('Bucket: %s' % bucket)
482 master = None
483 if bucket.startswith(MASTER_PREFIX):
484 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 for builder, tests in sorted(builders_and_tests.iteritems()):
486 print_text.append(' %s: %s' % (builder, tests))
487 parameters = {
488 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700490 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000491 'revision': options.revision,
492 }],
tandrii8c5a3532016-11-04 07:52:02 -0700493 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000494 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000495 if 'presubmit' in builder.lower():
496 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000497 if tests:
498 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700499
500 tags = [
501 'builder:%s' % builder,
502 'buildset:%s' % buildset,
503 'user_agent:git_cl_try',
504 ]
505 if master:
506 parameters['properties']['master'] = master
507 tags.append('master:%s' % master)
508
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509 batch_req_body['builds'].append(
510 {
511 'bucket': bucket,
512 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700514 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 }
516 )
517
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700519 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 http,
521 buildbucket_put_url,
522 'PUT',
523 body=json.dumps(batch_req_body),
524 headers={'Content-Type': 'application/json'}
525 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000526 print_text.append('To see results here, run: git cl try-results')
527 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700528 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000529
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000530
tandrii221ab252016-10-06 08:12:04 -0700531def fetch_try_jobs(auth_config, changelist, buildbucket_host,
532 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700533 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
qyearsley53f48a12016-09-01 10:45:13 -0700535 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536 """
tandrii221ab252016-10-06 08:12:04 -0700537 assert buildbucket_host
538 assert changelist.GetIssue(), 'CL must be uploaded first'
539 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
540 patchset = patchset or changelist.GetMostRecentPatchset()
541 assert patchset, 'CL must be uploaded first'
542
543 codereview_url = changelist.GetCodereviewServer()
544 codereview_host = urlparse.urlparse(codereview_url).hostname
545 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 if authenticator.has_cached_credentials():
547 http = authenticator.authorize(httplib2.Http())
548 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700549 print('Warning: Some results might be missing because %s' %
550 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700551 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 http = httplib2.Http()
553
554 http.force_exception_to_status_code = True
555
tandrii221ab252016-10-06 08:12:04 -0700556 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
557 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
558 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700560 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 params = {'tag': 'buildset:%s' % buildset}
562
563 builds = {}
564 while True:
565 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700566 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700568 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 for build in content.get('builds', []):
570 builds[build['id']] = build
571 if 'next_cursor' in content:
572 params['start_cursor'] = content['next_cursor']
573 else:
574 break
575 return builds
576
577
qyearsleyeab3c042016-08-24 09:18:28 -0700578def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 """Prints nicely result of fetch_try_jobs."""
580 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700581 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 return
583
584 # Make a copy, because we'll be modifying builds dictionary.
585 builds = builds.copy()
586 builder_names_cache = {}
587
588 def get_builder(b):
589 try:
590 return builder_names_cache[b['id']]
591 except KeyError:
592 try:
593 parameters = json.loads(b['parameters_json'])
594 name = parameters['builder_name']
595 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700596 print('WARNING: failed to get builder name for build %s: %s' % (
597 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 name = None
599 builder_names_cache[b['id']] = name
600 return name
601
602 def get_bucket(b):
603 bucket = b['bucket']
604 if bucket.startswith('master.'):
605 return bucket[len('master.'):]
606 return bucket
607
608 if options.print_master:
609 name_fmt = '%%-%ds %%-%ds' % (
610 max(len(str(get_bucket(b))) for b in builds.itervalues()),
611 max(len(str(get_builder(b))) for b in builds.itervalues()))
612 def get_name(b):
613 return name_fmt % (get_bucket(b), get_builder(b))
614 else:
615 name_fmt = '%%-%ds' % (
616 max(len(str(get_builder(b))) for b in builds.itervalues()))
617 def get_name(b):
618 return name_fmt % get_builder(b)
619
620 def sort_key(b):
621 return b['status'], b.get('result'), get_name(b), b.get('url')
622
623 def pop(title, f, color=None, **kwargs):
624 """Pop matching builds from `builds` dict and print them."""
625
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000626 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000627 colorize = str
628 else:
629 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
630
631 result = []
632 for b in builds.values():
633 if all(b.get(k) == v for k, v in kwargs.iteritems()):
634 builds.pop(b['id'])
635 result.append(b)
636 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700637 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700639 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640
641 total = len(builds)
642 pop(status='COMPLETED', result='SUCCESS',
643 title='Successes:', color=Fore.GREEN,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
646 title='Infra Failures:', color=Fore.MAGENTA,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
649 title='Failures:', color=Fore.RED,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='CANCELED',
652 title='Canceled:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b),))
654 pop(status='COMPLETED', result='FAILURE',
655 failure_reason='INVALID_BUILD_DEFINITION',
656 title='Wrong master/builder name:', color=Fore.MAGENTA,
657 f=lambda b: (get_name(b),))
658 pop(status='COMPLETED', result='FAILURE',
659 title='Other failures:',
660 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
661 pop(status='COMPLETED',
662 title='Other finished:',
663 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
664 pop(status='STARTED',
665 title='Started:', color=Fore.YELLOW,
666 f=lambda b: (get_name(b), b.get('url')))
667 pop(status='SCHEDULED',
668 title='Scheduled:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 # The last section is just in case buildbucket API changes OR there is a bug.
671 pop(title='Other:',
672 f=lambda b: (get_name(b), 'id=%s' % b['id']))
673 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700674 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000675
676
qyearsley53f48a12016-09-01 10:45:13 -0700677def write_try_results_json(output_file, builds):
678 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
679
680 The input |builds| dict is assumed to be generated by Buildbucket.
681 Buildbucket documentation: http://goo.gl/G0s101
682 """
683
684 def convert_build_dict(build):
685 return {
686 'buildbucket_id': build.get('id'),
687 'status': build.get('status'),
688 'result': build.get('result'),
689 'bucket': build.get('bucket'),
690 'builder_name': json.loads(
691 build.get('parameters_json', '{}')).get('builder_name'),
692 'failure_reason': build.get('failure_reason'),
693 'url': build.get('url'),
694 }
695
696 converted = []
697 for _, build in sorted(builds.items()):
698 converted.append(convert_build_dict(build))
699 write_json(output_file, converted)
700
701
iannucci@chromium.org79540052012-10-19 23:15:26 +0000702def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000703 """Prints statistics about the change to the user."""
704 # --no-ext-diff is broken in some versions of Git, so try to work around
705 # this by overriding the environment (but there is still a problem if the
706 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000707 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000708 if 'GIT_EXTERNAL_DIFF' in env:
709 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000710
711 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800712 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000713 else:
714 similarity_options = ['-M%s' % similarity]
715
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000716 try:
717 stdout = sys.stdout.fileno()
718 except AttributeError:
719 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000720 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000721 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000722 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000723 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000724
725
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000726class BuildbucketResponseException(Exception):
727 pass
728
729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730class Settings(object):
731 def __init__(self):
732 self.default_server = None
733 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000734 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 self.tree_status_url = None
736 self.viewvc_url = None
737 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000738 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000739 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000740 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000741 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000742 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000743 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000744 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745
746 def LazyUpdateIfNeeded(self):
747 """Updates the settings from a codereview.settings file, if available."""
748 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000749 # The only value that actually changes the behavior is
750 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000751 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000752 error_ok=True
753 ).strip().lower()
754
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000756 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 LoadCodereviewSettingsFromFile(cr_settings_file)
758 self.updated = True
759
760 def GetDefaultServerUrl(self, error_ok=False):
761 if not self.default_server:
762 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000763 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000764 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 if error_ok:
766 return self.default_server
767 if not self.default_server:
768 error_message = ('Could not find settings file. You must configure '
769 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000770 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772 return self.default_server
773
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000774 @staticmethod
775 def GetRelativeRoot():
776 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000779 if self.root is None:
780 self.root = os.path.abspath(self.GetRelativeRoot())
781 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000783 def GetGitMirror(self, remote='origin'):
784 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000785 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000786 if not os.path.isdir(local_url):
787 return None
788 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
789 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
790 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
791 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
792 if mirror.exists():
793 return mirror
794 return None
795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 def GetTreeStatusUrl(self, error_ok=False):
797 if not self.tree_status_url:
798 error_message = ('You must configure your tree status URL by running '
799 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self.tree_status_url = self._GetRietveldConfig(
801 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 return self.tree_status_url
803
804 def GetViewVCUrl(self):
805 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return self.viewvc_url
808
rmistry@google.com90752582014-01-14 21:04:50 +0000809 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000811
rmistry@google.com78948ed2015-07-08 23:09:57 +0000812 def GetIsSkipDependencyUpload(self, branch_name):
813 """Returns true if specified branch should skip dep uploads."""
814 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
815 error_ok=True)
816
rmistry@google.com5626a922015-02-26 14:03:30 +0000817 def GetRunPostUploadHook(self):
818 run_post_upload_hook = self._GetRietveldConfig(
819 'run-post-upload-hook', error_ok=True)
820 return run_post_upload_hook == "True"
821
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000822 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000823 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000824
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000825 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000826 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000827
ukai@chromium.orge8077812012-02-03 03:41:46 +0000828 def GetIsGerrit(self):
829 """Return true if this repo is assosiated with gerrit code review system."""
830 if self.is_gerrit is None:
831 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
832 return self.is_gerrit
833
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000834 def GetSquashGerritUploads(self):
835 """Return true if uploads to Gerrit should be squashed by default."""
836 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700837 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
838 if self.squash_gerrit_uploads is None:
839 # Default is squash now (http://crbug.com/611892#c23).
840 self.squash_gerrit_uploads = not (
841 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
842 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000843 return self.squash_gerrit_uploads
844
tandriia60502f2016-06-20 02:01:53 -0700845 def GetSquashGerritUploadsOverride(self):
846 """Return True or False if codereview.settings should be overridden.
847
848 Returns None if no override has been defined.
849 """
850 # See also http://crbug.com/611892#c23
851 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
852 error_ok=True).strip()
853 if result == 'true':
854 return True
855 if result == 'false':
856 return False
857 return None
858
tandrii@chromium.org28253532016-04-14 13:46:56 +0000859 def GetGerritSkipEnsureAuthenticated(self):
860 """Return True if EnsureAuthenticated should not be done for Gerrit
861 uploads."""
862 if self.gerrit_skip_ensure_authenticated is None:
863 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000864 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000865 error_ok=True).strip() == 'true')
866 return self.gerrit_skip_ensure_authenticated
867
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000868 def GetGitEditor(self):
869 """Return the editor specified in the git config, or None if none is."""
870 if self.git_editor is None:
871 self.git_editor = self._GetConfig('core.editor', error_ok=True)
872 return self.git_editor or None
873
thestig@chromium.org44202a22014-03-11 19:22:18 +0000874 def GetLintRegex(self):
875 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
876 DEFAULT_LINT_REGEX)
877
878 def GetLintIgnoreRegex(self):
879 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
880 DEFAULT_LINT_IGNORE_REGEX)
881
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000882 def GetProject(self):
883 if not self.project:
884 self.project = self._GetRietveldConfig('project', error_ok=True)
885 return self.project
886
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000887 def GetPendingRefPrefix(self):
888 if not self.pending_ref_prefix:
889 self.pending_ref_prefix = self._GetRietveldConfig(
890 'pending-ref-prefix', error_ok=True)
891 return self.pending_ref_prefix
892
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000893 def _GetRietveldConfig(self, param, **kwargs):
894 return self._GetConfig('rietveld.' + param, **kwargs)
895
rmistry@google.com78948ed2015-07-08 23:09:57 +0000896 def _GetBranchConfig(self, branch_name, param, **kwargs):
897 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899 def _GetConfig(self, param, **kwargs):
900 self.LazyUpdateIfNeeded()
901 return RunGit(['config', param], **kwargs).strip()
902
903
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100904class _GitNumbererState(object):
905 KNOWN_PROJECTS_WHITELIST = [
906 'chromium/src',
907 'external/webrtc',
908 'v8/v8',
909 ]
910
911 @classmethod
912 def load(cls, remote_url, remote_ref):
913 """Figures out the state by fetching special refs from remote repo.
914 """
915 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
916 url_parts = urlparse.urlparse(remote_url)
917 project_name = url_parts.path.lstrip('/').rstrip('git./')
918 for known in cls.KNOWN_PROJECTS_WHITELIST:
919 if project_name.endswith(known):
920 break
921 else:
922 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
923 return cls(cls._get_pending_prefix_fallback(), None)
924
Quinten Yearsley442fb642016-12-15 15:38:27 -0800925 # This pollutes local ref space, but the amount of objects is negligible.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100926 error, _ = cls._run_git_with_code([
927 'fetch', remote_url,
928 '+refs/meta/config:refs/git_cl/meta/config',
929 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
930 if error:
931 # Some ref doesn't exist or isn't accessible to current user.
932 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
933 # with git-numberer.
934 cls._warn('failed to fetch gnumbd and project config for %s: %s',
935 remote_url, error)
936 return cls(cls._get_pending_prefix_fallback(), None)
937 return cls(cls._get_pending_prefix(remote_ref),
938 cls._is_validator_enabled(remote_ref))
939
940 @classmethod
941 def _get_pending_prefix(cls, ref):
942 error, gnumbd_config_data = cls._run_git_with_code(
943 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
944 if error:
945 cls._warn('gnumbd config file not found')
946 return cls._get_pending_prefix_fallback()
947
948 try:
949 config = json.loads(gnumbd_config_data)
950 if cls.match_refglobs(ref, config['enabled_refglobs']):
951 return config['pending_ref_prefix']
952 return None
953 except KeyboardInterrupt:
954 raise
955 except Exception as e:
956 cls._warn('failed to parse gnumbd config: %s', e)
957 return cls._get_pending_prefix_fallback()
958
959 @staticmethod
960 def _get_pending_prefix_fallback():
961 global settings
962 if not settings:
963 settings = Settings()
964 return settings.GetPendingRefPrefix()
965
966 @classmethod
967 def _is_validator_enabled(cls, ref):
968 error, project_config_data = cls._run_git_with_code(
969 ['show', 'refs/git_cl/meta/config:project.config'])
970 if error:
971 cls._warn('project.config file not found')
972 return False
973 # Gerrit's project.config is really a git config file.
974 # So, parse it as such.
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000975 with gclient_utils.temporary_directory() as tempdir:
976 project_config_file = os.path.join(tempdir, 'project.config')
977 gclient_utils.FileWrite(project_config_file, project_config_data)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978
979 def get_opts(x):
980 code, out = cls._run_git_with_code(
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000981 ['config', '-f', project_config_file, '--get-all',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100982 'plugin.git-numberer.validate-%s-refglob' % x])
983 if code == 0:
984 return out.strip().splitlines()
985 return []
986 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000987
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100988 logging.info('validator config enabled %s disabled %s refglobs for '
989 '(this ref: %s)', enabled, disabled, ref)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100990
991 if cls.match_refglobs(ref, disabled):
992 return False
993 return cls.match_refglobs(ref, enabled)
994
995 @staticmethod
996 def match_refglobs(ref, refglobs):
997 for refglob in refglobs:
998 if ref == refglob or fnmatch.fnmatch(ref, refglob):
999 return True
1000 return False
1001
1002 @staticmethod
1003 def _run_git_with_code(*args, **kwargs):
1004 # The only reason for this wrapper is easy porting of this code to CQ
1005 # codebase, which forked git_cl.py and checkouts.py long time ago.
1006 return RunGitWithCode(*args, **kwargs)
1007
1008 @staticmethod
1009 def _warn(msg, *args):
1010 if args:
1011 msg = msg % args
1012 print('WARNING: %s' % msg)
1013
1014 def __init__(self, pending_prefix, validator_enabled):
1015 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001016 if pending_prefix:
1017 if not pending_prefix.endswith('/'):
1018 pending_prefix += '/'
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001019 self._pending_prefix = pending_prefix or None
1020 self._validator_enabled = validator_enabled or False
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01001021 logging.debug('_GitNumbererState(pending: %s, validator: %s)',
1022 self._pending_prefix, self._validator_enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001023
1024 @property
1025 def pending_prefix(self):
1026 return self._pending_prefix
1027
1028 @property
Andrii Shyshkalov8f15f3e2016-12-14 15:43:49 +01001029 def should_add_git_number(self):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001030 return self._validator_enabled and self._pending_prefix is None
1031
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033def ShortBranchName(branch):
1034 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001035 return branch.replace('refs/heads/', '', 1)
1036
1037
1038def GetCurrentBranchRef():
1039 """Returns branch ref (e.g., refs/heads/master) or None."""
1040 return RunGit(['symbolic-ref', 'HEAD'],
1041 stderr=subprocess2.VOID, error_ok=True).strip() or None
1042
1043
1044def GetCurrentBranch():
1045 """Returns current branch or None.
1046
1047 For refs/heads/* branches, returns just last part. For others, full ref.
1048 """
1049 branchref = GetCurrentBranchRef()
1050 if branchref:
1051 return ShortBranchName(branchref)
1052 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053
1054
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001055class _CQState(object):
1056 """Enum for states of CL with respect to Commit Queue."""
1057 NONE = 'none'
1058 DRY_RUN = 'dry_run'
1059 COMMIT = 'commit'
1060
1061 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1062
1063
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001064class _ParsedIssueNumberArgument(object):
1065 def __init__(self, issue=None, patchset=None, hostname=None):
1066 self.issue = issue
1067 self.patchset = patchset
1068 self.hostname = hostname
1069
1070 @property
1071 def valid(self):
1072 return self.issue is not None
1073
1074
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001075def ParseIssueNumberArgument(arg):
1076 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1077 fail_result = _ParsedIssueNumberArgument()
1078
1079 if arg.isdigit():
1080 return _ParsedIssueNumberArgument(issue=int(arg))
1081 if not arg.startswith('http'):
1082 return fail_result
1083 url = gclient_utils.UpgradeToHttps(arg)
1084 try:
1085 parsed_url = urlparse.urlparse(url)
1086 except ValueError:
1087 return fail_result
1088 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1089 tmp = cls.ParseIssueURL(parsed_url)
1090 if tmp is not None:
1091 return tmp
1092 return fail_result
1093
1094
Aaron Gablea45ee112016-11-22 15:14:38 -08001095class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001096 def __init__(self, issue, url):
1097 self.issue = issue
1098 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001099 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001100
1101 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001102 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001103 self.issue, self.url)
1104
1105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001107 """Changelist works with one changelist in local branch.
1108
1109 Supports two codereview backends: Rietveld or Gerrit, selected at object
1110 creation.
1111
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001112 Notes:
1113 * Not safe for concurrent multi-{thread,process} use.
1114 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001115 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 """
1117
1118 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1119 """Create a new ChangeList instance.
1120
1121 If issue is given, the codereview must be given too.
1122
1123 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1124 Otherwise, it's decided based on current configuration of the local branch,
1125 with default being 'rietveld' for backwards compatibility.
1126 See _load_codereview_impl for more details.
1127
1128 **kwargs will be passed directly to codereview implementation.
1129 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001131 global settings
1132 if not settings:
1133 # Happens when git_cl.py is used as a utility library.
1134 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001135
1136 if issue:
1137 assert codereview, 'codereview must be known, if issue is known'
1138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.branchref = branchref
1140 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001141 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 self.branch = ShortBranchName(self.branchref)
1143 else:
1144 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001146 self.lookedup_issue = False
1147 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 self.has_description = False
1149 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001150 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001152 self.cc = None
1153 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001154 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001155
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001158 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001159 assert self._codereview_impl
1160 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161
1162 def _load_codereview_impl(self, codereview=None, **kwargs):
1163 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001164 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1165 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1166 self._codereview = codereview
1167 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001168 return
1169
1170 # Automatic selection based on issue number set for a current branch.
1171 # Rietveld takes precedence over Gerrit.
1172 assert not self.issue
1173 # Whether we find issue or not, we are doing the lookup.
1174 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001175 if self.GetBranch():
1176 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1177 issue = _git_get_branch_config_value(
1178 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1179 if issue:
1180 self._codereview = codereview
1181 self._codereview_impl = cls(self, **kwargs)
1182 self.issue = int(issue)
1183 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184
1185 # No issue is set for this branch, so decide based on repo-wide settings.
1186 return self._load_codereview_impl(
1187 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1188 **kwargs)
1189
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001190 def IsGerrit(self):
1191 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192
1193 def GetCCList(self):
1194 """Return the users cc'd on this CL.
1195
agable92bec4f2016-08-24 09:27:27 -07001196 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001197 """
1198 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001199 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200 more_cc = ','.join(self.watchers)
1201 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1202 return self.cc
1203
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001204 def GetCCListWithoutDefault(self):
1205 """Return the users cc'd on this CL excluding default ones."""
1206 if self.cc is None:
1207 self.cc = ','.join(self.watchers)
1208 return self.cc
1209
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001210 def SetWatchers(self, watchers):
1211 """Set the list of email addresses that should be cc'd based on the changed
1212 files in this CL.
1213 """
1214 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetBranch(self):
1217 """Returns the short branch name, e.g. 'master'."""
1218 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001220 if not branchref:
1221 return None
1222 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 self.branch = ShortBranchName(self.branchref)
1224 return self.branch
1225
1226 def GetBranchRef(self):
1227 """Returns the full branch name, e.g. 'refs/heads/master'."""
1228 self.GetBranch() # Poke the lazy loader.
1229 return self.branchref
1230
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001231 def ClearBranch(self):
1232 """Clears cached branch data of this object."""
1233 self.branch = self.branchref = None
1234
tandrii5d48c322016-08-18 16:19:37 -07001235 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1236 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1237 kwargs['branch'] = self.GetBranch()
1238 return _git_get_branch_config_value(key, default, **kwargs)
1239
1240 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1241 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1242 assert self.GetBranch(), (
1243 'this CL must have an associated branch to %sset %s%s' %
1244 ('un' if value is None else '',
1245 key,
1246 '' if value is None else ' to %r' % value))
1247 kwargs['branch'] = self.GetBranch()
1248 return _git_set_branch_config_value(key, value, **kwargs)
1249
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001250 @staticmethod
1251 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001252 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 e.g. 'origin', 'refs/heads/master'
1254 """
1255 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001256 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1257
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001259 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001261 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1262 error_ok=True).strip()
1263 if upstream_branch:
1264 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001266 # Else, try to guess the origin remote.
1267 remote_branches = RunGit(['branch', '-r']).split()
1268 if 'origin/master' in remote_branches:
1269 # Fall back on origin/master if it exits.
1270 remote = 'origin'
1271 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001273 DieWithError(
1274 'Unable to determine default branch to diff against.\n'
1275 'Either pass complete "git diff"-style arguments, like\n'
1276 ' git cl upload origin/master\n'
1277 'or verify this branch is set up to track another \n'
1278 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279
1280 return remote, upstream_branch
1281
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001282 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001283 upstream_branch = self.GetUpstreamBranch()
1284 if not BranchExists(upstream_branch):
1285 DieWithError('The upstream for the current branch (%s) does not exist '
1286 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001287 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001288 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 def GetUpstreamBranch(self):
1291 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001294 upstream_branch = upstream_branch.replace('refs/heads/',
1295 'refs/remotes/%s/' % remote)
1296 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1297 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 self.upstream_branch = upstream_branch
1299 return self.upstream_branch
1300
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001302 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 remote, branch = None, self.GetBranch()
1304 seen_branches = set()
1305 while branch not in seen_branches:
1306 seen_branches.add(branch)
1307 remote, branch = self.FetchUpstreamTuple(branch)
1308 branch = ShortBranchName(branch)
1309 if remote != '.' or branch.startswith('refs/remotes'):
1310 break
1311 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 remotes = RunGit(['remote'], error_ok=True).split()
1313 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001317 logging.warn('Could not determine which remote this change is '
1318 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001319 else:
1320 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001321 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 branch = 'HEAD'
1323 if branch.startswith('refs/remotes'):
1324 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001325 elif branch.startswith('refs/branch-heads/'):
1326 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 else:
1328 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329 return self._remote
1330
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 def GitSanityChecks(self, upstream_git_obj):
1332 """Checks git repo status and ensures diff is from local commits."""
1333
sbc@chromium.org79706062015-01-14 21:18:12 +00001334 if upstream_git_obj is None:
1335 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001336 print('ERROR: unable to determine current branch (detached HEAD?)',
1337 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001339 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001340 return False
1341
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001342 # Verify the commit we're diffing against is in our current branch.
1343 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1344 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1345 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001346 print('ERROR: %s is not in the current branch. You may need to rebase '
1347 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001348 return False
1349
1350 # List the commits inside the diff, and verify they are all local.
1351 commits_in_diff = RunGit(
1352 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1353 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1354 remote_branch = remote_branch.strip()
1355 if code != 0:
1356 _, remote_branch = self.GetRemoteBranch()
1357
1358 commits_in_remote = RunGit(
1359 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1360
1361 common_commits = set(commits_in_diff) & set(commits_in_remote)
1362 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001363 print('ERROR: Your diff contains %d commits already in %s.\n'
1364 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1365 'the diff. If you are using a custom git flow, you can override'
1366 ' the reference used for this check with "git config '
1367 'gitcl.remotebranch <git-ref>".' % (
1368 len(common_commits), remote_branch, upstream_git_obj),
1369 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001370 return False
1371 return True
1372
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001373 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001374 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001375
1376 Returns None if it is not set.
1377 """
tandrii5d48c322016-08-18 16:19:37 -07001378 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001379
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 def GetRemoteUrl(self):
1381 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1382
1383 Returns None if there is no remote.
1384 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001385 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001386 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1387
1388 # If URL is pointing to a local directory, it is probably a git cache.
1389 if os.path.isdir(url):
1390 url = RunGit(['config', 'remote.%s.url' % remote],
1391 error_ok=True,
1392 cwd=url).strip()
1393 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001396 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001397 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001398 self.issue = self._GitGetBranchConfigValue(
1399 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 return self.issue
1402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 def GetIssueURL(self):
1404 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001405 issue = self.GetIssue()
1406 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001407 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001408 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
1410 def GetDescription(self, pretty=False):
1411 if not self.has_description:
1412 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001413 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 self.has_description = True
1415 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001416 # Set width to 72 columns + 2 space indent.
1417 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001419 lines = self.description.splitlines()
1420 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421 return self.description
1422
1423 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001424 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001425 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001426 self.patchset = self._GitGetBranchConfigValue(
1427 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001428 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 return self.patchset
1430
1431 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001432 """Set this branch's patchset. If patchset=0, clears the patchset."""
1433 assert self.GetBranch()
1434 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001435 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001436 else:
1437 self.patchset = int(patchset)
1438 self._GitSetBranchConfigValue(
1439 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001441 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001442 """Set this branch's issue. If issue isn't given, clears the issue."""
1443 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001445 issue = int(issue)
1446 self._GitSetBranchConfigValue(
1447 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001448 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001449 codereview_server = self._codereview_impl.GetCodereviewServer()
1450 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001451 self._GitSetBranchConfigValue(
1452 self._codereview_impl.CodereviewServerConfigKey(),
1453 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 else:
tandrii5d48c322016-08-18 16:19:37 -07001455 # Reset all of these just to be clean.
1456 reset_suffixes = [
1457 'last-upload-hash',
1458 self._codereview_impl.IssueConfigKey(),
1459 self._codereview_impl.PatchsetConfigKey(),
1460 self._codereview_impl.CodereviewServerConfigKey(),
1461 ] + self._PostUnsetIssueProperties()
1462 for prop in reset_suffixes:
1463 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001464 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001465 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466
dnjba1b0f32016-09-02 12:37:42 -07001467 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001468 if not self.GitSanityChecks(upstream_branch):
1469 DieWithError('\nGit sanity check failure')
1470
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001471 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001472 if not root:
1473 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001474 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001475
1476 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001477 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001478 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001479 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001480 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001481 except subprocess2.CalledProcessError:
1482 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001483 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001484 'This branch probably doesn\'t exist anymore. To reset the\n'
1485 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001486 ' git branch --set-upstream-to origin/master %s\n'
1487 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001488 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
maruel@chromium.org52424302012-08-29 15:14:30 +00001490 issue = self.GetIssue()
1491 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001492 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493 description = self.GetDescription()
1494 else:
1495 # If the change was never uploaded, use the log messages of all commits
1496 # up to the branch point, as git cl upload will prefill the description
1497 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001498 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1499 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001500
1501 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001502 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001503 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504 name,
1505 description,
1506 absroot,
1507 files,
1508 issue,
1509 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001510 author,
1511 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001512
dsansomee2d6fd92016-09-08 00:10:47 -07001513 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001514 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001515 return self._codereview_impl.UpdateDescriptionRemote(
1516 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001517
1518 def RunHook(self, committing, may_prompt, verbose, change):
1519 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1520 try:
1521 return presubmit_support.DoPresubmitChecks(change, committing,
1522 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1523 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001524 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1525 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001526 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001527 DieWithError(
1528 ('%s\nMaybe your depot_tools is out of date?\n'
1529 'If all fails, contact maruel@') % e)
1530
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001531 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1532 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001533 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1534 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001535 else:
1536 # Assume url.
1537 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1538 urlparse.urlparse(issue_arg))
1539 if not parsed_issue_arg or not parsed_issue_arg.valid:
1540 DieWithError('Failed to parse issue argument "%s". '
1541 'Must be an issue number or a valid URL.' % issue_arg)
1542 return self._codereview_impl.CMDPatchWithParsedIssue(
1543 parsed_issue_arg, reject, nocommit, directory)
1544
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 def CMDUpload(self, options, git_diff_args, orig_args):
1546 """Uploads a change to codereview."""
1547 if git_diff_args:
1548 # TODO(ukai): is it ok for gerrit case?
1549 base_branch = git_diff_args[0]
1550 else:
1551 if self.GetBranch() is None:
1552 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1553
1554 # Default to diffing against common ancestor of upstream branch
1555 base_branch = self.GetCommonAncestorWithUpstream()
1556 git_diff_args = [base_branch, 'HEAD']
1557
1558 # Make sure authenticated to codereview before running potentially expensive
1559 # hooks. It is a fast, best efforts check. Codereview still can reject the
1560 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001561 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562
1563 # Apply watchlists on upload.
1564 change = self.GetChange(base_branch, None)
1565 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1566 files = [f.LocalPath() for f in change.AffectedFiles()]
1567 if not options.bypass_watchlists:
1568 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1569
1570 if not options.bypass_hooks:
1571 if options.reviewers or options.tbr_owners:
1572 # Set the reviewer list now so that presubmit checks can access it.
1573 change_description = ChangeDescription(change.FullDescriptionText())
1574 change_description.update_reviewers(options.reviewers,
1575 options.tbr_owners,
1576 change)
1577 change.SetDescriptionText(change_description.description)
1578 hook_results = self.RunHook(committing=False,
1579 may_prompt=not options.force,
1580 verbose=options.verbose,
1581 change=change)
1582 if not hook_results.should_continue():
1583 return 1
1584 if not options.reviewers and hook_results.reviewers:
1585 options.reviewers = hook_results.reviewers.split(',')
1586
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001587 # TODO(tandrii): Checking local patchset against remote patchset is only
1588 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1589 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 latest_patchset = self.GetMostRecentPatchset()
1591 local_patchset = self.GetPatchset()
1592 if (latest_patchset and local_patchset and
1593 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001594 print('The last upload made from this repository was patchset #%d but '
1595 'the most recent patchset on the server is #%d.'
1596 % (local_patchset, latest_patchset))
1597 print('Uploading will still work, but if you\'ve uploaded to this '
1598 'issue from another machine or branch the patch you\'re '
1599 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600 ask_for_data('About to upload; enter to confirm.')
1601
1602 print_stats(options.similarity, options.find_copies, git_diff_args)
1603 ret = self.CMDUploadChange(options, git_diff_args, change)
1604 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001605 if options.use_commit_queue:
1606 self.SetCQState(_CQState.COMMIT)
1607 elif options.cq_dry_run:
1608 self.SetCQState(_CQState.DRY_RUN)
1609
tandrii5d48c322016-08-18 16:19:37 -07001610 _git_set_branch_config_value('last-upload-hash',
1611 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001612 # Run post upload hooks, if specified.
1613 if settings.GetRunPostUploadHook():
1614 presubmit_support.DoPostUploadExecuter(
1615 change,
1616 self,
1617 settings.GetRoot(),
1618 options.verbose,
1619 sys.stdout)
1620
1621 # Upload all dependencies if specified.
1622 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001623 print()
1624 print('--dependencies has been specified.')
1625 print('All dependent local branches will be re-uploaded.')
1626 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 # Remove the dependencies flag from args so that we do not end up in a
1628 # loop.
1629 orig_args.remove('--dependencies')
1630 ret = upload_branch_deps(self, orig_args)
1631 return ret
1632
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001633 def SetCQState(self, new_state):
1634 """Update the CQ state for latest patchset.
1635
1636 Issue must have been already uploaded and known.
1637 """
1638 assert new_state in _CQState.ALL_STATES
1639 assert self.GetIssue()
1640 return self._codereview_impl.SetCQState(new_state)
1641
qyearsley1fdfcb62016-10-24 13:22:03 -07001642 def TriggerDryRun(self):
1643 """Triggers a dry run and prints a warning on failure."""
1644 # TODO(qyearsley): Either re-use this method in CMDset_commit
1645 # and CMDupload, or change CMDtry to trigger dry runs with
1646 # just SetCQState, and catch keyboard interrupt and other
1647 # errors in that method.
1648 try:
1649 self.SetCQState(_CQState.DRY_RUN)
1650 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1651 return 0
1652 except KeyboardInterrupt:
1653 raise
1654 except:
1655 print('WARNING: failed to trigger CQ Dry Run.\n'
1656 'Either:\n'
1657 ' * your project has no CQ\n'
1658 ' * you don\'t have permission to trigger Dry Run\n'
1659 ' * bug in this code (see stack trace below).\n'
1660 'Consider specifying which bots to trigger manually '
1661 'or asking your project owners for permissions '
1662 'or contacting Chrome Infrastructure team at '
1663 'https://www.chromium.org/infra\n\n')
1664 # Still raise exception so that stack trace is printed.
1665 raise
1666
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001667 # Forward methods to codereview specific implementation.
1668
1669 def CloseIssue(self):
1670 return self._codereview_impl.CloseIssue()
1671
1672 def GetStatus(self):
1673 return self._codereview_impl.GetStatus()
1674
1675 def GetCodereviewServer(self):
1676 return self._codereview_impl.GetCodereviewServer()
1677
tandriide281ae2016-10-12 06:02:30 -07001678 def GetIssueOwner(self):
1679 """Get owner from codereview, which may differ from this checkout."""
1680 return self._codereview_impl.GetIssueOwner()
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 def GetApprovingReviewers(self):
1683 return self._codereview_impl.GetApprovingReviewers()
1684
1685 def GetMostRecentPatchset(self):
1686 return self._codereview_impl.GetMostRecentPatchset()
1687
tandriide281ae2016-10-12 06:02:30 -07001688 def CannotTriggerTryJobReason(self):
1689 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1690 return self._codereview_impl.CannotTriggerTryJobReason()
1691
tandrii8c5a3532016-11-04 07:52:02 -07001692 def GetTryjobProperties(self, patchset=None):
1693 """Returns dictionary of properties to launch tryjob."""
1694 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1695
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 def __getattr__(self, attr):
1697 # This is because lots of untested code accesses Rietveld-specific stuff
1698 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001699 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001700 # Note that child method defines __getattr__ as well, and forwards it here,
1701 # because _RietveldChangelistImpl is not cleaned up yet, and given
1702 # deprecation of Rietveld, it should probably be just removed.
1703 # Until that time, avoid infinite recursion by bypassing __getattr__
1704 # of implementation class.
1705 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001706
1707
1708class _ChangelistCodereviewBase(object):
1709 """Abstract base class encapsulating codereview specifics of a changelist."""
1710 def __init__(self, changelist):
1711 self._changelist = changelist # instance of Changelist
1712
1713 def __getattr__(self, attr):
1714 # Forward methods to changelist.
1715 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1716 # _RietveldChangelistImpl to avoid this hack?
1717 return getattr(self._changelist, attr)
1718
1719 def GetStatus(self):
1720 """Apply a rough heuristic to give a simple summary of an issue's review
1721 or CQ status, assuming adherence to a common workflow.
1722
1723 Returns None if no issue for this branch, or specific string keywords.
1724 """
1725 raise NotImplementedError()
1726
1727 def GetCodereviewServer(self):
1728 """Returns server URL without end slash, like "https://codereview.com"."""
1729 raise NotImplementedError()
1730
1731 def FetchDescription(self):
1732 """Fetches and returns description from the codereview server."""
1733 raise NotImplementedError()
1734
tandrii5d48c322016-08-18 16:19:37 -07001735 @classmethod
1736 def IssueConfigKey(cls):
1737 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001738 raise NotImplementedError()
1739
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001740 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001741 def PatchsetConfigKey(cls):
1742 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001743 raise NotImplementedError()
1744
tandrii5d48c322016-08-18 16:19:37 -07001745 @classmethod
1746 def CodereviewServerConfigKey(cls):
1747 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 raise NotImplementedError()
1749
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001750 def _PostUnsetIssueProperties(self):
1751 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001752 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001753
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 def GetRieveldObjForPresubmit(self):
1755 # This is an unfortunate Rietveld-embeddedness in presubmit.
1756 # For non-Rietveld codereviews, this probably should return a dummy object.
1757 raise NotImplementedError()
1758
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001759 def GetGerritObjForPresubmit(self):
1760 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1761 return None
1762
dsansomee2d6fd92016-09-08 00:10:47 -07001763 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 """Update the description on codereview site."""
1765 raise NotImplementedError()
1766
1767 def CloseIssue(self):
1768 """Closes the issue."""
1769 raise NotImplementedError()
1770
1771 def GetApprovingReviewers(self):
1772 """Returns a list of reviewers approving the change.
1773
1774 Note: not necessarily committers.
1775 """
1776 raise NotImplementedError()
1777
1778 def GetMostRecentPatchset(self):
1779 """Returns the most recent patchset number from the codereview site."""
1780 raise NotImplementedError()
1781
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001782 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1783 directory):
1784 """Fetches and applies the issue.
1785
1786 Arguments:
1787 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1788 reject: if True, reject the failed patch instead of switching to 3-way
1789 merge. Rietveld only.
1790 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1791 only.
1792 directory: switch to directory before applying the patch. Rietveld only.
1793 """
1794 raise NotImplementedError()
1795
1796 @staticmethod
1797 def ParseIssueURL(parsed_url):
1798 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1799 failed."""
1800 raise NotImplementedError()
1801
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001802 def EnsureAuthenticated(self, force):
1803 """Best effort check that user is authenticated with codereview server.
1804
1805 Arguments:
1806 force: whether to skip confirmation questions.
1807 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001808 raise NotImplementedError()
1809
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001810 def CMDUploadChange(self, options, args, change):
1811 """Uploads a change to codereview."""
1812 raise NotImplementedError()
1813
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001814 def SetCQState(self, new_state):
1815 """Update the CQ state for latest patchset.
1816
1817 Issue must have been already uploaded and known.
1818 """
1819 raise NotImplementedError()
1820
tandriie113dfd2016-10-11 10:20:12 -07001821 def CannotTriggerTryJobReason(self):
1822 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1823 raise NotImplementedError()
1824
tandriide281ae2016-10-12 06:02:30 -07001825 def GetIssueOwner(self):
1826 raise NotImplementedError()
1827
tandrii8c5a3532016-11-04 07:52:02 -07001828 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001829 raise NotImplementedError()
1830
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001831
1832class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1833 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1834 super(_RietveldChangelistImpl, self).__init__(changelist)
1835 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001836 if not rietveld_server:
1837 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838
1839 self._rietveld_server = rietveld_server
1840 self._auth_config = auth_config
1841 self._props = None
1842 self._rpc_server = None
1843
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844 def GetCodereviewServer(self):
1845 if not self._rietveld_server:
1846 # If we're on a branch then get the server potentially associated
1847 # with that branch.
1848 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001849 self._rietveld_server = gclient_utils.UpgradeToHttps(
1850 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001851 if not self._rietveld_server:
1852 self._rietveld_server = settings.GetDefaultServerUrl()
1853 return self._rietveld_server
1854
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001855 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001856 """Best effort check that user is authenticated with Rietveld server."""
1857 if self._auth_config.use_oauth2:
1858 authenticator = auth.get_authenticator_for_host(
1859 self.GetCodereviewServer(), self._auth_config)
1860 if not authenticator.has_cached_credentials():
1861 raise auth.LoginRequiredError(self.GetCodereviewServer())
1862
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001863 def FetchDescription(self):
1864 issue = self.GetIssue()
1865 assert issue
1866 try:
1867 return self.RpcServer().get_description(issue).strip()
1868 except urllib2.HTTPError as e:
1869 if e.code == 404:
1870 DieWithError(
1871 ('\nWhile fetching the description for issue %d, received a '
1872 '404 (not found)\n'
1873 'error. It is likely that you deleted this '
1874 'issue on the server. If this is the\n'
1875 'case, please run\n\n'
1876 ' git cl issue 0\n\n'
1877 'to clear the association with the deleted issue. Then run '
1878 'this command again.') % issue)
1879 else:
1880 DieWithError(
1881 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1882 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001883 print('Warning: Failed to retrieve CL description due to network '
1884 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 return ''
1886
1887 def GetMostRecentPatchset(self):
1888 return self.GetIssueProperties()['patchsets'][-1]
1889
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890 def GetIssueProperties(self):
1891 if self._props is None:
1892 issue = self.GetIssue()
1893 if not issue:
1894 self._props = {}
1895 else:
1896 self._props = self.RpcServer().get_issue_properties(issue, True)
1897 return self._props
1898
tandriie113dfd2016-10-11 10:20:12 -07001899 def CannotTriggerTryJobReason(self):
1900 props = self.GetIssueProperties()
1901 if not props:
1902 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1903 if props.get('closed'):
1904 return 'CL %s is closed' % self.GetIssue()
1905 if props.get('private'):
1906 return 'CL %s is private' % self.GetIssue()
1907 return None
1908
tandrii8c5a3532016-11-04 07:52:02 -07001909 def GetTryjobProperties(self, patchset=None):
1910 """Returns dictionary of properties to launch tryjob."""
1911 project = (self.GetIssueProperties() or {}).get('project')
1912 return {
1913 'issue': self.GetIssue(),
1914 'patch_project': project,
1915 'patch_storage': 'rietveld',
1916 'patchset': patchset or self.GetPatchset(),
1917 'rietveld': self.GetCodereviewServer(),
1918 }
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def GetApprovingReviewers(self):
1921 return get_approving_reviewers(self.GetIssueProperties())
1922
tandriide281ae2016-10-12 06:02:30 -07001923 def GetIssueOwner(self):
1924 return (self.GetIssueProperties() or {}).get('owner_email')
1925
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 def AddComment(self, message):
1927 return self.RpcServer().add_comment(self.GetIssue(), message)
1928
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001929 def GetStatus(self):
1930 """Apply a rough heuristic to give a simple summary of an issue's review
1931 or CQ status, assuming adherence to a common workflow.
1932
1933 Returns None if no issue for this branch, or one of the following keywords:
1934 * 'error' - error from review tool (including deleted issues)
1935 * 'unsent' - not sent for review
1936 * 'waiting' - waiting for review
1937 * 'reply' - waiting for owner to reply to review
1938 * 'lgtm' - LGTM from at least one approved reviewer
1939 * 'commit' - in the commit queue
1940 * 'closed' - closed
1941 """
1942 if not self.GetIssue():
1943 return None
1944
1945 try:
1946 props = self.GetIssueProperties()
1947 except urllib2.HTTPError:
1948 return 'error'
1949
1950 if props.get('closed'):
1951 # Issue is closed.
1952 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001953 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001954 # Issue is in the commit queue.
1955 return 'commit'
1956
1957 try:
1958 reviewers = self.GetApprovingReviewers()
1959 except urllib2.HTTPError:
1960 return 'error'
1961
1962 if reviewers:
1963 # Was LGTM'ed.
1964 return 'lgtm'
1965
1966 messages = props.get('messages') or []
1967
tandrii9d2c7a32016-06-22 03:42:45 -07001968 # Skip CQ messages that don't require owner's action.
1969 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1970 if 'Dry run:' in messages[-1]['text']:
1971 messages.pop()
1972 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1973 # This message always follows prior messages from CQ,
1974 # so skip this too.
1975 messages.pop()
1976 else:
1977 # This is probably a CQ messages warranting user attention.
1978 break
1979
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001980 if not messages:
1981 # No message was sent.
1982 return 'unsent'
1983 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001984 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001985 return 'reply'
1986 return 'waiting'
1987
dsansomee2d6fd92016-09-08 00:10:47 -07001988 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001989 return self.RpcServer().update_description(
1990 self.GetIssue(), self.description)
1991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001993 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001995 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001996 return self.SetFlags({flag: value})
1997
1998 def SetFlags(self, flags):
1999 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002000 """
phajdan.jr68598232016-08-10 03:28:28 -07002001 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002002 try:
tandrii4b233bd2016-07-06 03:50:29 -07002003 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002004 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002005 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002006 if e.code == 404:
2007 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2008 if e.code == 403:
2009 DieWithError(
2010 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002011 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002012 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002013
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002014 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 """Returns an upload.RpcServer() to access this review's rietveld instance.
2016 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002017 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002018 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002019 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002020 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002021 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002023 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002024 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002025 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026
tandrii5d48c322016-08-18 16:19:37 -07002027 @classmethod
2028 def PatchsetConfigKey(cls):
2029 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030
tandrii5d48c322016-08-18 16:19:37 -07002031 @classmethod
2032 def CodereviewServerConfigKey(cls):
2033 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002034
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002035 def GetRieveldObjForPresubmit(self):
2036 return self.RpcServer()
2037
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002038 def SetCQState(self, new_state):
2039 props = self.GetIssueProperties()
2040 if props.get('private'):
2041 DieWithError('Cannot set-commit on private issue')
2042
2043 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002044 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002045 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002046 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002047 else:
tandrii4b233bd2016-07-06 03:50:29 -07002048 assert new_state == _CQState.DRY_RUN
2049 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002050
2051
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002052 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2053 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002054 # PatchIssue should never be called with a dirty tree. It is up to the
2055 # caller to check this, but just in case we assert here since the
2056 # consequences of the caller not checking this could be dire.
2057 assert(not git_common.is_dirty_git_tree('apply'))
2058 assert(parsed_issue_arg.valid)
2059 self._changelist.issue = parsed_issue_arg.issue
2060 if parsed_issue_arg.hostname:
2061 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2062
skobes6468b902016-10-24 08:45:10 -07002063 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2064 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2065 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002066 try:
skobes6468b902016-10-24 08:45:10 -07002067 scm_obj.apply_patch(patchset_object)
2068 except Exception as e:
2069 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002070 return 1
2071
2072 # If we had an issue, commit the current state and register the issue.
2073 if not nocommit:
2074 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2075 'patch from issue %(i)s at patchset '
2076 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2077 % {'i': self.GetIssue(), 'p': patchset})])
2078 self.SetIssue(self.GetIssue())
2079 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002080 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002082 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002083 return 0
2084
2085 @staticmethod
2086 def ParseIssueURL(parsed_url):
2087 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2088 return None
wychen3c1c1722016-08-04 11:46:36 -07002089 # Rietveld patch: https://domain/<number>/#ps<patchset>
2090 match = re.match(r'/(\d+)/$', parsed_url.path)
2091 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2092 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002093 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002094 issue=int(match.group(1)),
2095 patchset=int(match2.group(1)),
2096 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002097 # Typical url: https://domain/<issue_number>[/[other]]
2098 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2099 if match:
skobes6468b902016-10-24 08:45:10 -07002100 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002101 issue=int(match.group(1)),
2102 hostname=parsed_url.netloc)
2103 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2104 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2105 if match:
skobes6468b902016-10-24 08:45:10 -07002106 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002107 issue=int(match.group(1)),
2108 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002109 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002110 return None
2111
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002112 def CMDUploadChange(self, options, args, change):
2113 """Upload the patch to Rietveld."""
2114 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2115 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002116 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2117 if options.emulate_svn_auto_props:
2118 upload_args.append('--emulate_svn_auto_props')
2119
2120 change_desc = None
2121
2122 if options.email is not None:
2123 upload_args.extend(['--email', options.email])
2124
2125 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002126 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002127 upload_args.extend(['--title', options.title])
2128 if options.message:
2129 upload_args.extend(['--message', options.message])
2130 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002131 print('This branch is associated with issue %s. '
2132 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 else:
nodirca166002016-06-27 10:59:51 -07002134 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002135 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002136 if options.message:
2137 message = options.message
2138 else:
2139 message = CreateDescriptionFromLog(args)
2140 if options.title:
2141 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002142 change_desc = ChangeDescription(message)
2143 if options.reviewers or options.tbr_owners:
2144 change_desc.update_reviewers(options.reviewers,
2145 options.tbr_owners,
2146 change)
2147 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002148 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149
2150 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002151 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002152 return 1
2153
2154 upload_args.extend(['--message', change_desc.description])
2155 if change_desc.get_reviewers():
2156 upload_args.append('--reviewers=%s' % ','.join(
2157 change_desc.get_reviewers()))
2158 if options.send_mail:
2159 if not change_desc.get_reviewers():
2160 DieWithError("Must specify reviewers to send email.")
2161 upload_args.append('--send_mail')
2162
2163 # We check this before applying rietveld.private assuming that in
2164 # rietveld.cc only addresses which we can send private CLs to are listed
2165 # if rietveld.private is set, and so we should ignore rietveld.cc only
2166 # when --private is specified explicitly on the command line.
2167 if options.private:
2168 logging.warn('rietveld.cc is ignored since private flag is specified. '
2169 'You need to review and add them manually if necessary.')
2170 cc = self.GetCCListWithoutDefault()
2171 else:
2172 cc = self.GetCCList()
2173 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002174 if change_desc.get_cced():
2175 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002176 if cc:
2177 upload_args.extend(['--cc', cc])
2178
2179 if options.private or settings.GetDefaultPrivateFlag() == "True":
2180 upload_args.append('--private')
2181
2182 upload_args.extend(['--git_similarity', str(options.similarity)])
2183 if not options.find_copies:
2184 upload_args.extend(['--git_no_find_copies'])
2185
2186 # Include the upstream repo's URL in the change -- this is useful for
2187 # projects that have their source spread across multiple repos.
2188 remote_url = self.GetGitBaseUrlFromConfig()
2189 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002190 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2191 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2192 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 remote, remote_branch = self.GetRemoteBranch()
2195 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002196 pending_prefix_check=True,
2197 remote_url=self.GetRemoteUrl())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198 if target_ref:
2199 upload_args.extend(['--target_ref', target_ref])
2200
2201 # Look for dependent patchsets. See crbug.com/480453 for more details.
2202 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2203 upstream_branch = ShortBranchName(upstream_branch)
2204 if remote is '.':
2205 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002206 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002208 print()
2209 print('Skipping dependency patchset upload because git config '
2210 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2211 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002212 else:
2213 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002214 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002215 auth_config=auth_config)
2216 branch_cl_issue_url = branch_cl.GetIssueURL()
2217 branch_cl_issue = branch_cl.GetIssue()
2218 branch_cl_patchset = branch_cl.GetPatchset()
2219 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2220 upload_args.extend(
2221 ['--depends_on_patchset', '%s:%s' % (
2222 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002223 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 '\n'
2225 'The current branch (%s) is tracking a local branch (%s) with '
2226 'an associated CL.\n'
2227 'Adding %s/#ps%s as a dependency patchset.\n'
2228 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2229 branch_cl_patchset))
2230
2231 project = settings.GetProject()
2232 if project:
2233 upload_args.extend(['--project', project])
2234
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002235 try:
2236 upload_args = ['upload'] + upload_args + args
2237 logging.info('upload.RealMain(%s)', upload_args)
2238 issue, patchset = upload.RealMain(upload_args)
2239 issue = int(issue)
2240 patchset = int(patchset)
2241 except KeyboardInterrupt:
2242 sys.exit(1)
2243 except:
2244 # If we got an exception after the user typed a description for their
2245 # change, back up the description before re-raising.
2246 if change_desc:
2247 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2248 print('\nGot exception while uploading -- saving description to %s\n' %
2249 backup_path)
2250 backup_file = open(backup_path, 'w')
2251 backup_file.write(change_desc.description)
2252 backup_file.close()
2253 raise
2254
2255 if not self.GetIssue():
2256 self.SetIssue(issue)
2257 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002258 return 0
2259
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260
2261class _GerritChangelistImpl(_ChangelistCodereviewBase):
2262 def __init__(self, changelist, auth_config=None):
2263 # auth_config is Rietveld thing, kept here to preserve interface only.
2264 super(_GerritChangelistImpl, self).__init__(changelist)
2265 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002266 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002267 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002268 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002269
2270 def _GetGerritHost(self):
2271 # Lazy load of configs.
2272 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002273 if self._gerrit_host and '.' not in self._gerrit_host:
2274 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2275 # This happens for internal stuff http://crbug.com/614312.
2276 parsed = urlparse.urlparse(self.GetRemoteUrl())
2277 if parsed.scheme == 'sso':
2278 print('WARNING: using non https URLs for remote is likely broken\n'
2279 ' Your current remote is: %s' % self.GetRemoteUrl())
2280 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2281 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002282 return self._gerrit_host
2283
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002284 def _GetGitHost(self):
2285 """Returns git host to be used when uploading change to Gerrit."""
2286 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2287
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002288 def GetCodereviewServer(self):
2289 if not self._gerrit_server:
2290 # If we're on a branch then get the server potentially associated
2291 # with that branch.
2292 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002293 self._gerrit_server = self._GitGetBranchConfigValue(
2294 self.CodereviewServerConfigKey())
2295 if self._gerrit_server:
2296 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002297 if not self._gerrit_server:
2298 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2299 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002300 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002301 parts[0] = parts[0] + '-review'
2302 self._gerrit_host = '.'.join(parts)
2303 self._gerrit_server = 'https://%s' % self._gerrit_host
2304 return self._gerrit_server
2305
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002306 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002307 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002308 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002309
tandrii5d48c322016-08-18 16:19:37 -07002310 @classmethod
2311 def PatchsetConfigKey(cls):
2312 return 'gerritpatchset'
2313
2314 @classmethod
2315 def CodereviewServerConfigKey(cls):
2316 return 'gerritserver'
2317
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002318 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002319 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002320 if settings.GetGerritSkipEnsureAuthenticated():
2321 # For projects with unusual authentication schemes.
2322 # See http://crbug.com/603378.
2323 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002324 # Lazy-loader to identify Gerrit and Git hosts.
2325 if gerrit_util.GceAuthenticator.is_gce():
2326 return
2327 self.GetCodereviewServer()
2328 git_host = self._GetGitHost()
2329 assert self._gerrit_server and self._gerrit_host
2330 cookie_auth = gerrit_util.CookiesAuthenticator()
2331
2332 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2333 git_auth = cookie_auth.get_auth_header(git_host)
2334 if gerrit_auth and git_auth:
2335 if gerrit_auth == git_auth:
2336 return
2337 print((
2338 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2339 ' Check your %s or %s file for credentials of hosts:\n'
2340 ' %s\n'
2341 ' %s\n'
2342 ' %s') %
2343 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2344 git_host, self._gerrit_host,
2345 cookie_auth.get_new_password_message(git_host)))
2346 if not force:
2347 ask_for_data('If you know what you are doing, press Enter to continue, '
2348 'Ctrl+C to abort.')
2349 return
2350 else:
2351 missing = (
2352 [] if gerrit_auth else [self._gerrit_host] +
2353 [] if git_auth else [git_host])
2354 DieWithError('Credentials for the following hosts are required:\n'
2355 ' %s\n'
2356 'These are read from %s (or legacy %s)\n'
2357 '%s' % (
2358 '\n '.join(missing),
2359 cookie_auth.get_gitcookies_path(),
2360 cookie_auth.get_netrc_path(),
2361 cookie_auth.get_new_password_message(git_host)))
2362
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002363 def _PostUnsetIssueProperties(self):
2364 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002365 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002366
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002367 def GetRieveldObjForPresubmit(self):
2368 class ThisIsNotRietveldIssue(object):
2369 def __nonzero__(self):
2370 # This is a hack to make presubmit_support think that rietveld is not
2371 # defined, yet still ensure that calls directly result in a decent
2372 # exception message below.
2373 return False
2374
2375 def __getattr__(self, attr):
2376 print(
2377 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2378 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2379 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2380 'or use Rietveld for codereview.\n'
2381 'See also http://crbug.com/579160.' % attr)
2382 raise NotImplementedError()
2383 return ThisIsNotRietveldIssue()
2384
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002385 def GetGerritObjForPresubmit(self):
2386 return presubmit_support.GerritAccessor(self._GetGerritHost())
2387
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002388 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002389 """Apply a rough heuristic to give a simple summary of an issue's review
2390 or CQ status, assuming adherence to a common workflow.
2391
2392 Returns None if no issue for this branch, or one of the following keywords:
2393 * 'error' - error from review tool (including deleted issues)
2394 * 'unsent' - no reviewers added
2395 * 'waiting' - waiting for review
2396 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002397 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002398 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002399 * 'commit' - in the commit queue
2400 * 'closed' - abandoned
2401 """
2402 if not self.GetIssue():
2403 return None
2404
2405 try:
2406 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002407 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002408 return 'error'
2409
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002410 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002411 return 'closed'
2412
2413 cq_label = data['labels'].get('Commit-Queue', {})
2414 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002415 votes = cq_label.get('all', [])
2416 highest_vote = 0
2417 for v in votes:
2418 highest_vote = max(highest_vote, v.get('value', 0))
2419 vote_value = str(highest_vote)
2420 if vote_value != '0':
2421 # Add a '+' if the value is not 0 to match the values in the label.
2422 # The cq_label does not have negatives.
2423 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424 vote_text = cq_label.get('values', {}).get(vote_value, '')
2425 if vote_text.lower() == 'commit':
2426 return 'commit'
2427
2428 lgtm_label = data['labels'].get('Code-Review', {})
2429 if lgtm_label:
2430 if 'rejected' in lgtm_label:
2431 return 'not lgtm'
2432 if 'approved' in lgtm_label:
2433 return 'lgtm'
2434
2435 if not data.get('reviewers', {}).get('REVIEWER', []):
2436 return 'unsent'
2437
2438 messages = data.get('messages', [])
2439 if messages:
2440 owner = data['owner'].get('_account_id')
2441 last_message_author = messages[-1].get('author', {}).get('_account_id')
2442 if owner != last_message_author:
2443 # Some reply from non-owner.
2444 return 'reply'
2445
2446 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002447
2448 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002449 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002450 return data['revisions'][data['current_revision']]['_number']
2451
2452 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002453 data = self._GetChangeDetail(['CURRENT_REVISION'])
2454 current_rev = data['current_revision']
2455 url = data['revisions'][current_rev]['fetch']['http']['url']
2456 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002457
dsansomee2d6fd92016-09-08 00:10:47 -07002458 def UpdateDescriptionRemote(self, description, force=False):
2459 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2460 if not force:
2461 ask_for_data(
2462 'The description cannot be modified while the issue has a pending '
2463 'unpublished edit. Either publish the edit in the Gerrit web UI '
2464 'or delete it.\n\n'
2465 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2466
2467 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2468 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002469 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002470 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002471
2472 def CloseIssue(self):
2473 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2474
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002475 def GetApprovingReviewers(self):
2476 """Returns a list of reviewers approving the change.
2477
2478 Note: not necessarily committers.
2479 """
2480 raise NotImplementedError()
2481
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002482 def SubmitIssue(self, wait_for_merge=True):
2483 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2484 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002485
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002486 def _GetChangeDetail(self, options=None, issue=None):
2487 options = options or []
2488 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002489 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002490 try:
2491 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2492 options, ignore_404=False)
2493 except gerrit_util.GerritError as e:
2494 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002495 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002496 raise
tandriic2405f52016-10-10 08:13:15 -07002497 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002498
agable32978d92016-11-01 12:55:02 -07002499 def _GetChangeCommit(self, issue=None):
2500 issue = issue or self.GetIssue()
2501 assert issue, 'issue is required to query Gerrit'
2502 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2503 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002504 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002505 return data
2506
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002507 def CMDLand(self, force, bypass_hooks, verbose):
2508 if git_common.is_dirty_git_tree('land'):
2509 return 1
tandriid60367b2016-06-22 05:25:12 -07002510 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2511 if u'Commit-Queue' in detail.get('labels', {}):
2512 if not force:
2513 ask_for_data('\nIt seems this repository has a Commit Queue, '
2514 'which can test and land changes for you. '
2515 'Are you sure you wish to bypass it?\n'
2516 'Press Enter to continue, Ctrl+C to abort.')
2517
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002518 differs = True
tandriic4344b52016-08-29 06:04:54 -07002519 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520 # Note: git diff outputs nothing if there is no diff.
2521 if not last_upload or RunGit(['diff', last_upload]).strip():
2522 print('WARNING: some changes from local branch haven\'t been uploaded')
2523 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002524 if detail['current_revision'] == last_upload:
2525 differs = False
2526 else:
2527 print('WARNING: local branch contents differ from latest uploaded '
2528 'patchset')
2529 if differs:
2530 if not force:
2531 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002532 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2533 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002534 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2535 elif not bypass_hooks:
2536 hook_results = self.RunHook(
2537 committing=True,
2538 may_prompt=not force,
2539 verbose=verbose,
2540 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2541 if not hook_results.should_continue():
2542 return 1
2543
2544 self.SubmitIssue(wait_for_merge=True)
2545 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002546 links = self._GetChangeCommit().get('web_links', [])
2547 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002548 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002549 print('Landed as %s' % link.get('url'))
2550 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002551 return 0
2552
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002553 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2554 directory):
2555 assert not reject
2556 assert not nocommit
2557 assert not directory
2558 assert parsed_issue_arg.valid
2559
2560 self._changelist.issue = parsed_issue_arg.issue
2561
2562 if parsed_issue_arg.hostname:
2563 self._gerrit_host = parsed_issue_arg.hostname
2564 self._gerrit_server = 'https://%s' % self._gerrit_host
2565
tandriic2405f52016-10-10 08:13:15 -07002566 try:
2567 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002568 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002569 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002570
2571 if not parsed_issue_arg.patchset:
2572 # Use current revision by default.
2573 revision_info = detail['revisions'][detail['current_revision']]
2574 patchset = int(revision_info['_number'])
2575 else:
2576 patchset = parsed_issue_arg.patchset
2577 for revision_info in detail['revisions'].itervalues():
2578 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2579 break
2580 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002581 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002582 (parsed_issue_arg.patchset, self.GetIssue()))
2583
2584 fetch_info = revision_info['fetch']['http']
2585 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2586 RunGit(['cherry-pick', 'FETCH_HEAD'])
2587 self.SetIssue(self.GetIssue())
2588 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002589 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002590 (self.GetIssue(), self.GetPatchset()))
2591 return 0
2592
2593 @staticmethod
2594 def ParseIssueURL(parsed_url):
2595 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2596 return None
2597 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2598 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2599 # Short urls like https://domain/<issue_number> can be used, but don't allow
2600 # specifying the patchset (you'd 404), but we allow that here.
2601 if parsed_url.path == '/':
2602 part = parsed_url.fragment
2603 else:
2604 part = parsed_url.path
2605 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2606 if match:
2607 return _ParsedIssueNumberArgument(
2608 issue=int(match.group(2)),
2609 patchset=int(match.group(4)) if match.group(4) else None,
2610 hostname=parsed_url.netloc)
2611 return None
2612
tandrii16e0b4e2016-06-07 10:34:28 -07002613 def _GerritCommitMsgHookCheck(self, offer_removal):
2614 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2615 if not os.path.exists(hook):
2616 return
2617 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2618 # custom developer made one.
2619 data = gclient_utils.FileRead(hook)
2620 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2621 return
2622 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002623 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002624 'and may interfere with it in subtle ways.\n'
2625 'We recommend you remove the commit-msg hook.')
2626 if offer_removal:
2627 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2628 if reply.lower().startswith('y'):
2629 gclient_utils.rm_file_or_tree(hook)
2630 print('Gerrit commit-msg hook removed.')
2631 else:
2632 print('OK, will keep Gerrit commit-msg hook in place.')
2633
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002634 def CMDUploadChange(self, options, args, change):
2635 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002636 if options.squash and options.no_squash:
2637 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002638
2639 if not options.squash and not options.no_squash:
2640 # Load default for user, repo, squash=true, in this order.
2641 options.squash = settings.GetSquashGerritUploads()
2642 elif options.no_squash:
2643 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002644
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002645 # We assume the remote called "origin" is the one we want.
2646 # It is probably not worthwhile to support different workflows.
2647 gerrit_remote = 'origin'
2648
2649 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002650 # Gerrit will not support pending prefix at all.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002651 branch = GetTargetRef(remote, remote_branch, options.target_branch,
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01002652 pending_prefix_check=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002653
Aaron Gableb56ad332017-01-06 15:24:31 -08002654 # This may be None; default fallback value is determined in logic below.
2655 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002656 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002657
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002658 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002659 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 if self.GetIssue():
2661 # Try to get the message from a previous upload.
2662 message = self.GetDescription()
2663 if not message:
2664 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002665 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002666 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002667 if not title:
2668 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2669 title = ask_for_data(
2670 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002671 if title == default_title:
2672 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 change_id = self._GetChangeDetail()['change_id']
2674 while True:
2675 footer_change_ids = git_footers.get_footer_change_id(message)
2676 if footer_change_ids == [change_id]:
2677 break
2678 if not footer_change_ids:
2679 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002680 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681 continue
2682 # There is already a valid footer but with different or several ids.
2683 # Doing this automatically is non-trivial as we don't want to lose
2684 # existing other footers, yet we want to append just 1 desired
2685 # Change-Id. Thus, just create a new footer, but let user verify the
2686 # new description.
2687 message = '%s\n\nChange-Id: %s' % (message, change_id)
2688 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002689 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002690 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002691 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692 'Please, check the proposed correction to the description, '
2693 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2694 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2695 change_id))
2696 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2697 if not options.force:
2698 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002699 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 message = change_desc.description
2701 if not message:
2702 DieWithError("Description is empty. Aborting...")
2703 # Continue the while loop.
2704 # Sanity check of this code - we should end up with proper message
2705 # footer.
2706 assert [change_id] == git_footers.get_footer_change_id(message)
2707 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002708 else: # if not self.GetIssue()
2709 if options.message:
2710 message = options.message
2711 else:
2712 message = CreateDescriptionFromLog(args)
2713 if options.title:
2714 message = options.title + '\n\n' + message
2715 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002716 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002717 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002718 # On first upload, patchset title is always this string, while
2719 # --title flag gets converted to first line of message.
2720 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002721 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722 if not change_desc.description:
2723 DieWithError("Description is empty. Aborting...")
2724 message = change_desc.description
2725 change_ids = git_footers.get_footer_change_id(message)
2726 if len(change_ids) > 1:
2727 DieWithError('too many Change-Id footers, at most 1 allowed.')
2728 if not change_ids:
2729 # Generate the Change-Id automatically.
2730 message = git_footers.add_footer_change_id(
2731 message, GenerateGerritChangeId(message))
2732 change_desc.set_description(message)
2733 change_ids = git_footers.get_footer_change_id(message)
2734 assert len(change_ids) == 1
2735 change_id = change_ids[0]
2736
2737 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2738 if remote is '.':
2739 # If our upstream branch is local, we base our squashed commit on its
2740 # squashed version.
2741 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2742 # Check the squashed hash of the parent.
2743 parent = RunGit(['config',
2744 'branch.%s.gerritsquashhash' % upstream_branch_name],
2745 error_ok=True).strip()
2746 # Verify that the upstream branch has been uploaded too, otherwise
2747 # Gerrit will create additional CLs when uploading.
2748 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2749 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002750 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002751 '\nUpload upstream branch %s first.\n'
2752 'It is likely that this branch has been rebased since its last '
2753 'upload, so you just need to upload it again.\n'
2754 '(If you uploaded it with --no-squash, then branch dependencies '
2755 'are not supported, and you should reupload with --squash.)'
2756 % upstream_branch_name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 else:
2758 parent = self.GetCommonAncestorWithUpstream()
2759
2760 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2761 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2762 '-m', message]).strip()
2763 else:
2764 change_desc = ChangeDescription(
2765 options.message or CreateDescriptionFromLog(args))
2766 if not change_desc.description:
2767 DieWithError("Description is empty. Aborting...")
2768
2769 if not git_footers.get_footer_change_id(change_desc.description):
2770 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002771 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2772 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002773 ref_to_push = 'HEAD'
2774 parent = '%s/%s' % (gerrit_remote, branch)
2775 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2776
2777 assert change_desc
2778 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2779 ref_to_push)]).splitlines()
2780 if len(commits) > 1:
2781 print('WARNING: This will upload %d commits. Run the following command '
2782 'to see which commits will be uploaded: ' % len(commits))
2783 print('git log %s..%s' % (parent, ref_to_push))
2784 print('You can also use `git squash-branch` to squash these into a '
2785 'single commit.')
2786 ask_for_data('About to upload; enter to confirm.')
2787
2788 if options.reviewers or options.tbr_owners:
2789 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2790 change)
2791
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002792 # Extra options that can be specified at push time. Doc:
2793 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2794 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002795 if change_desc.get_reviewers(tbr_only=True):
2796 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2797 refspec_opts.append('l=Code-Review+1')
2798
Aaron Gable9b713dd2016-12-14 16:04:21 -08002799 if title:
2800 if not re.match(r'^[\w ]+$', title):
2801 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002802 if not automatic_title:
2803 print('WARNING: Patchset title may only contain alphanumeric chars '
2804 'and spaces. Cleaned up title:\n%s' % title)
2805 if not options.force:
2806 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002807 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2808 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002809 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002810
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002811 if options.send_mail:
2812 if not change_desc.get_reviewers():
2813 DieWithError('Must specify reviewers to send email.')
2814 refspec_opts.append('notify=ALL')
2815 else:
2816 refspec_opts.append('notify=NONE')
2817
tandrii99a72f22016-08-17 14:33:24 -07002818 reviewers = change_desc.get_reviewers()
2819 if reviewers:
2820 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002821
agablec6787972016-09-09 16:13:34 -07002822 if options.private:
2823 refspec_opts.append('draft')
2824
rmistry9eadede2016-09-19 11:22:43 -07002825 if options.topic:
2826 # Documentation on Gerrit topics is here:
2827 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2828 refspec_opts.append('topic=%s' % options.topic)
2829
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002830 refspec_suffix = ''
2831 if refspec_opts:
2832 refspec_suffix = '%' + ','.join(refspec_opts)
2833 assert ' ' not in refspec_suffix, (
2834 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002835 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002836
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002837 try:
2838 push_stdout = gclient_utils.CheckCallAndFilter(
2839 ['git', 'push', gerrit_remote, refspec],
2840 print_stdout=True,
2841 # Flush after every line: useful for seeing progress when running as
2842 # recipe.
2843 filter_fn=lambda _: sys.stdout.flush())
2844 except subprocess2.CalledProcessError:
2845 DieWithError('Failed to create a change. Please examine output above '
2846 'for the reason of the failure. ')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002847
2848 if options.squash:
2849 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2850 change_numbers = [m.group(1)
2851 for m in map(regex.match, push_stdout.splitlines())
2852 if m]
2853 if len(change_numbers) != 1:
2854 DieWithError(
2855 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2856 'Change-Id: %s') % (len(change_numbers), change_id))
2857 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002858 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002859
2860 # Add cc's from the CC_LIST and --cc flag (if any).
2861 cc = self.GetCCList().split(',')
2862 if options.cc:
2863 cc.extend(options.cc)
2864 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002865 if change_desc.get_cced():
2866 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002867 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002868 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002869 self._GetGerritHost(), self.GetIssue(), cc,
2870 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002871 return 0
2872
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002873 def _AddChangeIdToCommitMessage(self, options, args):
2874 """Re-commits using the current message, assumes the commit hook is in
2875 place.
2876 """
2877 log_desc = options.message or CreateDescriptionFromLog(args)
2878 git_command = ['commit', '--amend', '-m', log_desc]
2879 RunGit(git_command)
2880 new_log_desc = CreateDescriptionFromLog(args)
2881 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002882 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002883 return new_log_desc
2884 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002885 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002887 def SetCQState(self, new_state):
2888 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002889 vote_map = {
2890 _CQState.NONE: 0,
2891 _CQState.DRY_RUN: 1,
2892 _CQState.COMMIT : 2,
2893 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002894 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2895 if new_state == _CQState.DRY_RUN:
2896 # Don't spam everybody reviewer/owner.
2897 kwargs['notify'] = 'NONE'
2898 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002899
tandriie113dfd2016-10-11 10:20:12 -07002900 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002901 try:
2902 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002903 except GerritChangeNotExists:
2904 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002905
2906 if data['status'] in ('ABANDONED', 'MERGED'):
2907 return 'CL %s is closed' % self.GetIssue()
2908
2909 def GetTryjobProperties(self, patchset=None):
2910 """Returns dictionary of properties to launch tryjob."""
2911 data = self._GetChangeDetail(['ALL_REVISIONS'])
2912 patchset = int(patchset or self.GetPatchset())
2913 assert patchset
2914 revision_data = None # Pylint wants it to be defined.
2915 for revision_data in data['revisions'].itervalues():
2916 if int(revision_data['_number']) == patchset:
2917 break
2918 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002919 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002920 (patchset, self.GetIssue()))
2921 return {
2922 'patch_issue': self.GetIssue(),
2923 'patch_set': patchset or self.GetPatchset(),
2924 'patch_project': data['project'],
2925 'patch_storage': 'gerrit',
2926 'patch_ref': revision_data['fetch']['http']['ref'],
2927 'patch_repository_url': revision_data['fetch']['http']['url'],
2928 'patch_gerrit_url': self.GetCodereviewServer(),
2929 }
tandriie113dfd2016-10-11 10:20:12 -07002930
tandriide281ae2016-10-12 06:02:30 -07002931 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002932 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002933
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002934
2935_CODEREVIEW_IMPLEMENTATIONS = {
2936 'rietveld': _RietveldChangelistImpl,
2937 'gerrit': _GerritChangelistImpl,
2938}
2939
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002940
iannuccie53c9352016-08-17 14:40:40 -07002941def _add_codereview_issue_select_options(parser, extra=""):
2942 _add_codereview_select_options(parser)
2943
2944 text = ('Operate on this issue number instead of the current branch\'s '
2945 'implicit issue.')
2946 if extra:
2947 text += ' '+extra
2948 parser.add_option('-i', '--issue', type=int, help=text)
2949
2950
2951def _process_codereview_issue_select_options(parser, options):
2952 _process_codereview_select_options(parser, options)
2953 if options.issue is not None and not options.forced_codereview:
2954 parser.error('--issue must be specified with either --rietveld or --gerrit')
2955
2956
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002957def _add_codereview_select_options(parser):
2958 """Appends --gerrit and --rietveld options to force specific codereview."""
2959 parser.codereview_group = optparse.OptionGroup(
2960 parser, 'EXPERIMENTAL! Codereview override options')
2961 parser.add_option_group(parser.codereview_group)
2962 parser.codereview_group.add_option(
2963 '--gerrit', action='store_true',
2964 help='Force the use of Gerrit for codereview')
2965 parser.codereview_group.add_option(
2966 '--rietveld', action='store_true',
2967 help='Force the use of Rietveld for codereview')
2968
2969
2970def _process_codereview_select_options(parser, options):
2971 if options.gerrit and options.rietveld:
2972 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2973 options.forced_codereview = None
2974 if options.gerrit:
2975 options.forced_codereview = 'gerrit'
2976 elif options.rietveld:
2977 options.forced_codereview = 'rietveld'
2978
2979
tandriif9aefb72016-07-01 09:06:51 -07002980def _get_bug_line_values(default_project, bugs):
2981 """Given default_project and comma separated list of bugs, yields bug line
2982 values.
2983
2984 Each bug can be either:
2985 * a number, which is combined with default_project
2986 * string, which is left as is.
2987
2988 This function may produce more than one line, because bugdroid expects one
2989 project per line.
2990
2991 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2992 ['v8:123', 'chromium:789']
2993 """
2994 default_bugs = []
2995 others = []
2996 for bug in bugs.split(','):
2997 bug = bug.strip()
2998 if bug:
2999 try:
3000 default_bugs.append(int(bug))
3001 except ValueError:
3002 others.append(bug)
3003
3004 if default_bugs:
3005 default_bugs = ','.join(map(str, default_bugs))
3006 if default_project:
3007 yield '%s:%s' % (default_project, default_bugs)
3008 else:
3009 yield default_bugs
3010 for other in sorted(others):
3011 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3012 yield other
3013
3014
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003015class ChangeDescription(object):
3016 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003017 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003018 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003019 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003020 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003021
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003022 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024
agable@chromium.org42c20792013-09-12 17:34:49 +00003025 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003026 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 return '\n'.join(self._description_lines)
3028
3029 def set_description(self, desc):
3030 if isinstance(desc, basestring):
3031 lines = desc.splitlines()
3032 else:
3033 lines = [line.rstrip() for line in desc]
3034 while lines and not lines[0]:
3035 lines.pop(0)
3036 while lines and not lines[-1]:
3037 lines.pop(-1)
3038 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003039
piman@chromium.org336f9122014-09-04 02:16:55 +00003040 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003041 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003042 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003043 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003044 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003045 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003046
agable@chromium.org42c20792013-09-12 17:34:49 +00003047 # Get the set of R= and TBR= lines and remove them from the desciption.
3048 regexp = re.compile(self.R_LINE)
3049 matches = [regexp.match(line) for line in self._description_lines]
3050 new_desc = [l for i, l in enumerate(self._description_lines)
3051 if not matches[i]]
3052 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003053
agable@chromium.org42c20792013-09-12 17:34:49 +00003054 # Construct new unified R= and TBR= lines.
3055 r_names = []
3056 tbr_names = []
3057 for match in matches:
3058 if not match:
3059 continue
3060 people = cleanup_list([match.group(2).strip()])
3061 if match.group(1) == 'TBR':
3062 tbr_names.extend(people)
3063 else:
3064 r_names.extend(people)
3065 for name in r_names:
3066 if name not in reviewers:
3067 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003068 if add_owners_tbr:
3069 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003070 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003071 all_reviewers = set(tbr_names + reviewers)
3072 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3073 all_reviewers)
3074 tbr_names.extend(owners_db.reviewers_for(missing_files,
3075 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3077 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3078
3079 # Put the new lines in the description where the old first R= line was.
3080 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3081 if 0 <= line_loc < len(self._description_lines):
3082 if new_tbr_line:
3083 self._description_lines.insert(line_loc, new_tbr_line)
3084 if new_r_line:
3085 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003086 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003087 if new_r_line:
3088 self.append_footer(new_r_line)
3089 if new_tbr_line:
3090 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003091
tandriif9aefb72016-07-01 09:06:51 -07003092 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 self.set_description([
3095 '# Enter a description of the change.',
3096 '# This will be displayed on the codereview site.',
3097 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003098 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003099 '--------------------',
3100 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003101
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 regexp = re.compile(self.BUG_LINE)
3103 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003104 prefix = settings.GetBugPrefix()
3105 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3106 for value in values:
3107 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3108 self.append_footer('BUG=%s' % value)
3109
agable@chromium.org42c20792013-09-12 17:34:49 +00003110 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003111 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003112 if not content:
3113 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003114 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003115
3116 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003117 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3118 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003119 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003120 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003121
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003122 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003123 """Adds a footer line to the description.
3124
3125 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3126 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3127 that Gerrit footers are always at the end.
3128 """
3129 parsed_footer_line = git_footers.parse_footer(line)
3130 if parsed_footer_line:
3131 # Line is a gerrit footer in the form: Footer-Key: any value.
3132 # Thus, must be appended observing Gerrit footer rules.
3133 self.set_description(
3134 git_footers.add_footer(self.description,
3135 key=parsed_footer_line[0],
3136 value=parsed_footer_line[1]))
3137 return
3138
3139 if not self._description_lines:
3140 self._description_lines.append(line)
3141 return
3142
3143 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3144 if gerrit_footers:
3145 # git_footers.split_footers ensures that there is an empty line before
3146 # actual (gerrit) footers, if any. We have to keep it that way.
3147 assert top_lines and top_lines[-1] == ''
3148 top_lines, separator = top_lines[:-1], top_lines[-1:]
3149 else:
3150 separator = [] # No need for separator if there are no gerrit_footers.
3151
3152 prev_line = top_lines[-1] if top_lines else ''
3153 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3154 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3155 top_lines.append('')
3156 top_lines.append(line)
3157 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003158
tandrii99a72f22016-08-17 14:33:24 -07003159 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003160 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003161 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003162 reviewers = [match.group(2).strip()
3163 for match in matches
3164 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003165 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003166
bradnelsond975b302016-10-23 12:20:23 -07003167 def get_cced(self):
3168 """Retrieves the list of reviewers."""
3169 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3170 cced = [match.group(2).strip() for match in matches if match]
3171 return cleanup_list(cced)
3172
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003173 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3174 """Updates this commit description given the parent.
3175
3176 This is essentially what Gnumbd used to do.
3177 Consult https://goo.gl/WMmpDe for more details.
3178 """
3179 assert parent_msg # No, orphan branch creation isn't supported.
3180 assert parent_hash
3181 assert dest_ref
3182 parent_footer_map = git_footers.parse_footers(parent_msg)
3183 # This will also happily parse svn-position, which GnumbD is no longer
3184 # supporting. While we'd generate correct footers, the verifier plugin
3185 # installed in Gerrit will block such commit (ie git push below will fail).
3186 parent_position = git_footers.get_position(parent_footer_map)
3187
3188 # Cherry-picks may have last line obscuring their prior footers,
3189 # from git_footers perspective. This is also what Gnumbd did.
3190 cp_line = None
3191 if (self._description_lines and
3192 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3193 cp_line = self._description_lines.pop()
3194
3195 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3196
3197 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3198 # user interference with actual footers we'd insert below.
3199 for i, (k, v) in enumerate(parsed_footers):
3200 if k.startswith('Cr-'):
3201 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3202
3203 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003204 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003205 if parent_position[0] == dest_ref:
3206 # Same branch as parent.
3207 number = int(parent_position[1]) + 1
3208 else:
3209 number = 1 # New branch, and extra lineage.
3210 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3211 int(parent_position[1])))
3212
3213 parsed_footers.append(('Cr-Commit-Position',
3214 '%s@{#%d}' % (dest_ref, number)))
3215 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3216
3217 self._description_lines = top_lines
3218 if cp_line:
3219 self._description_lines.append(cp_line)
3220 if self._description_lines[-1] != '':
3221 self._description_lines.append('') # Ensure footer separator.
3222 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3223
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003224
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003225def get_approving_reviewers(props):
3226 """Retrieves the reviewers that approved a CL from the issue properties with
3227 messages.
3228
3229 Note that the list may contain reviewers that are not committer, thus are not
3230 considered by the CQ.
3231 """
3232 return sorted(
3233 set(
3234 message['sender']
3235 for message in props['messages']
3236 if message['approval'] and message['sender'] in props['reviewers']
3237 )
3238 )
3239
3240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241def FindCodereviewSettingsFile(filename='codereview.settings'):
3242 """Finds the given file starting in the cwd and going up.
3243
3244 Only looks up to the top of the repository unless an
3245 'inherit-review-settings-ok' file exists in the root of the repository.
3246 """
3247 inherit_ok_file = 'inherit-review-settings-ok'
3248 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003249 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3251 root = '/'
3252 while True:
3253 if filename in os.listdir(cwd):
3254 if os.path.isfile(os.path.join(cwd, filename)):
3255 return open(os.path.join(cwd, filename))
3256 if cwd == root:
3257 break
3258 cwd = os.path.dirname(cwd)
3259
3260
3261def LoadCodereviewSettingsFromFile(fileobj):
3262 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003263 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 def SetProperty(name, setting, unset_error_ok=False):
3266 fullname = 'rietveld.' + name
3267 if setting in keyvals:
3268 RunGit(['config', fullname, keyvals[setting]])
3269 else:
3270 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3271
tandrii48df5812016-10-17 03:55:37 -07003272 if not keyvals.get('GERRIT_HOST', False):
3273 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003274 # Only server setting is required. Other settings can be absent.
3275 # In that case, we ignore errors raised during option deletion attempt.
3276 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003277 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3279 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003280 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003281 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3282 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003283 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003284 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003285 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3286 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003287
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003288 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003289 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003290
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003291 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003292 RunGit(['config', 'gerrit.squash-uploads',
3293 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003294
tandrii@chromium.org28253532016-04-14 13:46:56 +00003295 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003296 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003297 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003299 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3300 #should be of the form
3301 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3302 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3303 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3304 keyvals['ORIGIN_URL_CONFIG']])
3305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003306
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003307def urlretrieve(source, destination):
3308 """urllib is broken for SSL connections via a proxy therefore we
3309 can't use urllib.urlretrieve()."""
3310 with open(destination, 'w') as f:
3311 f.write(urllib2.urlopen(source).read())
3312
3313
ukai@chromium.org712d6102013-11-27 00:52:58 +00003314def hasSheBang(fname):
3315 """Checks fname is a #! script."""
3316 with open(fname) as f:
3317 return f.read(2).startswith('#!')
3318
3319
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003320# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3321def DownloadHooks(*args, **kwargs):
3322 pass
3323
3324
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003325def DownloadGerritHook(force):
3326 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003327
3328 Args:
3329 force: True to update hooks. False to install hooks if not present.
3330 """
3331 if not settings.GetIsGerrit():
3332 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003333 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003334 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3335 if not os.access(dst, os.X_OK):
3336 if os.path.exists(dst):
3337 if not force:
3338 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003339 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003340 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003341 if not hasSheBang(dst):
3342 DieWithError('Not a script: %s\n'
3343 'You need to download from\n%s\n'
3344 'into .git/hooks/commit-msg and '
3345 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003346 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3347 except Exception:
3348 if os.path.exists(dst):
3349 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003350 DieWithError('\nFailed to download hooks.\n'
3351 'You need to download from\n%s\n'
3352 'into .git/hooks/commit-msg and '
3353 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003354
3355
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003356
3357def GetRietveldCodereviewSettingsInteractively():
3358 """Prompt the user for settings."""
3359 server = settings.GetDefaultServerUrl(error_ok=True)
3360 prompt = 'Rietveld server (host[:port])'
3361 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3362 newserver = ask_for_data(prompt + ':')
3363 if not server and not newserver:
3364 newserver = DEFAULT_SERVER
3365 if newserver:
3366 newserver = gclient_utils.UpgradeToHttps(newserver)
3367 if newserver != server:
3368 RunGit(['config', 'rietveld.server', newserver])
3369
3370 def SetProperty(initial, caption, name, is_url):
3371 prompt = caption
3372 if initial:
3373 prompt += ' ("x" to clear) [%s]' % initial
3374 new_val = ask_for_data(prompt + ':')
3375 if new_val == 'x':
3376 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3377 elif new_val:
3378 if is_url:
3379 new_val = gclient_utils.UpgradeToHttps(new_val)
3380 if new_val != initial:
3381 RunGit(['config', 'rietveld.' + name, new_val])
3382
3383 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3384 SetProperty(settings.GetDefaultPrivateFlag(),
3385 'Private flag (rietveld only)', 'private', False)
3386 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3387 'tree-status-url', False)
3388 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3389 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3390 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3391 'run-post-upload-hook', False)
3392
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003393@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003394def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003395 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003396
tandrii5d0a0422016-09-14 06:24:35 -07003397 print('WARNING: git cl config works for Rietveld only')
3398 # TODO(tandrii): remove this once we switch to Gerrit.
3399 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003400 parser.add_option('--activate-update', action='store_true',
3401 help='activate auto-updating [rietveld] section in '
3402 '.git/config')
3403 parser.add_option('--deactivate-update', action='store_true',
3404 help='deactivate auto-updating [rietveld] section in '
3405 '.git/config')
3406 options, args = parser.parse_args(args)
3407
3408 if options.deactivate_update:
3409 RunGit(['config', 'rietveld.autoupdate', 'false'])
3410 return
3411
3412 if options.activate_update:
3413 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3414 return
3415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003416 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003417 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003418 return 0
3419
3420 url = args[0]
3421 if not url.endswith('codereview.settings'):
3422 url = os.path.join(url, 'codereview.settings')
3423
3424 # Load code review settings and download hooks (if available).
3425 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3426 return 0
3427
3428
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003429def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003430 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003431 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3432 branch = ShortBranchName(branchref)
3433 _, args = parser.parse_args(args)
3434 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003435 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003436 return RunGit(['config', 'branch.%s.base-url' % branch],
3437 error_ok=False).strip()
3438 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003439 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003440 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3441 error_ok=False).strip()
3442
3443
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003444def color_for_status(status):
3445 """Maps a Changelist status to color, for CMDstatus and other tools."""
3446 return {
3447 'unsent': Fore.RED,
3448 'waiting': Fore.BLUE,
3449 'reply': Fore.YELLOW,
3450 'lgtm': Fore.GREEN,
3451 'commit': Fore.MAGENTA,
3452 'closed': Fore.CYAN,
3453 'error': Fore.WHITE,
3454 }.get(status, Fore.WHITE)
3455
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003456
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003457def get_cl_statuses(changes, fine_grained, max_processes=None):
3458 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003459
3460 If fine_grained is true, this will fetch CL statuses from the server.
3461 Otherwise, simply indicate if there's a matching url for the given branches.
3462
3463 If max_processes is specified, it is used as the maximum number of processes
3464 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3465 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003466
3467 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003468 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003469 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003470 upload.verbosity = 0
3471
3472 if fine_grained:
3473 # Process one branch synchronously to work through authentication, then
3474 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003475 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003476 def fetch(cl):
3477 try:
3478 return (cl, cl.GetStatus())
3479 except:
3480 # See http://crbug.com/629863.
3481 logging.exception('failed to fetch status for %s:', cl)
3482 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003483 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003484
tandriiea9514a2016-08-17 12:32:37 -07003485 changes_to_fetch = changes[1:]
3486 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003487 # Exit early if there was only one branch to fetch.
3488 return
3489
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003490 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003491 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003492 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003493 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003494
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003495 fetched_cls = set()
3496 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003497 while True:
3498 try:
3499 row = it.next(timeout=5)
3500 except multiprocessing.TimeoutError:
3501 break
3502
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003503 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003504 yield row
3505
3506 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003507 for cl in set(changes_to_fetch) - fetched_cls:
3508 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003509
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003510 else:
3511 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003512 for cl in changes:
3513 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003514
rmistry@google.com2dd99862015-06-22 12:22:18 +00003515
3516def upload_branch_deps(cl, args):
3517 """Uploads CLs of local branches that are dependents of the current branch.
3518
3519 If the local branch dependency tree looks like:
3520 test1 -> test2.1 -> test3.1
3521 -> test3.2
3522 -> test2.2 -> test3.3
3523
3524 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3525 run on the dependent branches in this order:
3526 test2.1, test3.1, test3.2, test2.2, test3.3
3527
3528 Note: This function does not rebase your local dependent branches. Use it when
3529 you make a change to the parent branch that will not conflict with its
3530 dependent branches, and you would like their dependencies updated in
3531 Rietveld.
3532 """
3533 if git_common.is_dirty_git_tree('upload-branch-deps'):
3534 return 1
3535
3536 root_branch = cl.GetBranch()
3537 if root_branch is None:
3538 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3539 'Get on a branch!')
3540 if not cl.GetIssue() or not cl.GetPatchset():
3541 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3542 'patchset dependencies without an uploaded CL.')
3543
3544 branches = RunGit(['for-each-ref',
3545 '--format=%(refname:short) %(upstream:short)',
3546 'refs/heads'])
3547 if not branches:
3548 print('No local branches found.')
3549 return 0
3550
3551 # Create a dictionary of all local branches to the branches that are dependent
3552 # on it.
3553 tracked_to_dependents = collections.defaultdict(list)
3554 for b in branches.splitlines():
3555 tokens = b.split()
3556 if len(tokens) == 2:
3557 branch_name, tracked = tokens
3558 tracked_to_dependents[tracked].append(branch_name)
3559
vapiera7fbd5a2016-06-16 09:17:49 -07003560 print()
3561 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003562 dependents = []
3563 def traverse_dependents_preorder(branch, padding=''):
3564 dependents_to_process = tracked_to_dependents.get(branch, [])
3565 padding += ' '
3566 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003568 dependents.append(dependent)
3569 traverse_dependents_preorder(dependent, padding)
3570 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003572
3573 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003575 return 0
3576
vapiera7fbd5a2016-06-16 09:17:49 -07003577 print('This command will checkout all dependent branches and run '
3578 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003579 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3580
andybons@chromium.org962f9462016-02-03 20:00:42 +00003581 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003582 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003583 args.extend(['-t', 'Updated patchset dependency'])
3584
rmistry@google.com2dd99862015-06-22 12:22:18 +00003585 # Record all dependents that failed to upload.
3586 failures = {}
3587 # Go through all dependents, checkout the branch and upload.
3588 try:
3589 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print()
3591 print('--------------------------------------')
3592 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003593 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003595 try:
3596 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003597 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003598 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003599 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003600 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003601 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602 finally:
3603 # Swap back to the original root branch.
3604 RunGit(['checkout', '-q', root_branch])
3605
vapiera7fbd5a2016-06-16 09:17:49 -07003606 print()
3607 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003608 for dependent_branch in dependents:
3609 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003610 print(' %s : %s' % (dependent_branch, upload_status))
3611 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003612
3613 return 0
3614
3615
kmarshall3bff56b2016-06-06 18:31:47 -07003616def CMDarchive(parser, args):
3617 """Archives and deletes branches associated with closed changelists."""
3618 parser.add_option(
3619 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003620 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003621 parser.add_option(
3622 '-f', '--force', action='store_true',
3623 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003624 parser.add_option(
3625 '-d', '--dry-run', action='store_true',
3626 help='Skip the branch tagging and removal steps.')
3627 parser.add_option(
3628 '-t', '--notags', action='store_true',
3629 help='Do not tag archived branches. '
3630 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003631
3632 auth.add_auth_options(parser)
3633 options, args = parser.parse_args(args)
3634 if args:
3635 parser.error('Unsupported args: %s' % ' '.join(args))
3636 auth_config = auth.extract_auth_config_from_options(options)
3637
3638 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3639 if not branches:
3640 return 0
3641
vapiera7fbd5a2016-06-16 09:17:49 -07003642 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003643 changes = [Changelist(branchref=b, auth_config=auth_config)
3644 for b in branches.splitlines()]
3645 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3646 statuses = get_cl_statuses(changes,
3647 fine_grained=True,
3648 max_processes=options.maxjobs)
3649 proposal = [(cl.GetBranch(),
3650 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3651 for cl, status in statuses
3652 if status == 'closed']
3653 proposal.sort()
3654
3655 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003656 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003657 return 0
3658
3659 current_branch = GetCurrentBranch()
3660
vapiera7fbd5a2016-06-16 09:17:49 -07003661 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003662 if options.notags:
3663 for next_item in proposal:
3664 print(' ' + next_item[0])
3665 else:
3666 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3667 for next_item in proposal:
3668 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003669
kmarshall9249e012016-08-23 12:02:16 -07003670 # Quit now on precondition failure or if instructed by the user, either
3671 # via an interactive prompt or by command line flags.
3672 if options.dry_run:
3673 print('\nNo changes were made (dry run).\n')
3674 return 0
3675 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003676 print('You are currently on a branch \'%s\' which is associated with a '
3677 'closed codereview issue, so archive cannot proceed. Please '
3678 'checkout another branch and run this command again.' %
3679 current_branch)
3680 return 1
kmarshall9249e012016-08-23 12:02:16 -07003681 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003682 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3683 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003684 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003685 return 1
3686
3687 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003688 if not options.notags:
3689 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003690 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003691
vapiera7fbd5a2016-06-16 09:17:49 -07003692 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003693
3694 return 0
3695
3696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003698 """Show status of changelists.
3699
3700 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003701 - Red not sent for review or broken
3702 - Blue waiting for review
3703 - Yellow waiting for you to reply to review
3704 - Green LGTM'ed
3705 - Magenta in the commit queue
3706 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003707
3708 Also see 'git cl comments'.
3709 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003711 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003712 parser.add_option('-f', '--fast', action='store_true',
3713 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003714 parser.add_option(
3715 '-j', '--maxjobs', action='store', type=int,
3716 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003717
3718 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003719 _add_codereview_issue_select_options(
3720 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003721 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003722 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003723 if args:
3724 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003725 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003726
iannuccie53c9352016-08-17 14:40:40 -07003727 if options.issue is not None and not options.field:
3728 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003731 cl = Changelist(auth_config=auth_config, issue=options.issue,
3732 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003733 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003734 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735 elif options.field == 'id':
3736 issueid = cl.GetIssue()
3737 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003738 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003739 elif options.field == 'patch':
3740 patchset = cl.GetPatchset()
3741 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003742 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003743 elif options.field == 'status':
3744 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003745 elif options.field == 'url':
3746 url = cl.GetIssueURL()
3747 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003748 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003749 return 0
3750
3751 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3752 if not branches:
3753 print('No local branch found.')
3754 return 0
3755
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003756 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003757 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003758 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003759 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003760 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003761 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003762 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003763
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003764 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003765 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3766 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3767 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003768 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003769 c, status = output.next()
3770 branch_statuses[c.GetBranch()] = status
3771 status = branch_statuses.pop(branch)
3772 url = cl.GetIssueURL()
3773 if url and (not status or status == 'error'):
3774 # The issue probably doesn't exist anymore.
3775 url += ' (broken)'
3776
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003777 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003778 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003779 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003780 color = ''
3781 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003782 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003783 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003784 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003785 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003786
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003787 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print()
3789 print('Current branch:',)
3790 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003791 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003792 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003793 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003794 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003795 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003796 print('Issue description:')
3797 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003798 return 0
3799
3800
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003801def colorize_CMDstatus_doc():
3802 """To be called once in main() to add colors to git cl status help."""
3803 colors = [i for i in dir(Fore) if i[0].isupper()]
3804
3805 def colorize_line(line):
3806 for color in colors:
3807 if color in line.upper():
3808 # Extract whitespaces first and the leading '-'.
3809 indent = len(line) - len(line.lstrip(' ')) + 1
3810 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3811 return line
3812
3813 lines = CMDstatus.__doc__.splitlines()
3814 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3815
3816
phajdan.jre328cf92016-08-22 04:12:17 -07003817def write_json(path, contents):
3818 with open(path, 'w') as f:
3819 json.dump(contents, f)
3820
3821
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003822@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003824 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825
3826 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003827 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003828 parser.add_option('-r', '--reverse', action='store_true',
3829 help='Lookup the branch(es) for the specified issues. If '
3830 'no issues are specified, all branches with mapped '
3831 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003832 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003833 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003834 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003835 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003836
dnj@chromium.org406c4402015-03-03 17:22:28 +00003837 if options.reverse:
3838 branches = RunGit(['for-each-ref', 'refs/heads',
3839 '--format=%(refname:short)']).splitlines()
3840
3841 # Reverse issue lookup.
3842 issue_branch_map = {}
3843 for branch in branches:
3844 cl = Changelist(branchref=branch)
3845 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3846 if not args:
3847 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003848 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003849 for issue in args:
3850 if not issue:
3851 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003852 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print('Branch for issue number %s: %s' % (
3854 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003855 if options.json:
3856 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003857 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003858 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003859 if len(args) > 0:
3860 try:
3861 issue = int(args[0])
3862 except ValueError:
3863 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003864 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003865 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003866 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003867 if options.json:
3868 write_json(options.json, {
3869 'issue': cl.GetIssue(),
3870 'issue_url': cl.GetIssueURL(),
3871 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003872 return 0
3873
3874
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003875def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003876 """Shows or posts review comments for any changelist."""
3877 parser.add_option('-a', '--add-comment', dest='comment',
3878 help='comment to add to an issue')
3879 parser.add_option('-i', dest='issue',
3880 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003881 parser.add_option('-j', '--json-file',
3882 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003883 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003884 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003885 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003886
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003887 issue = None
3888 if options.issue:
3889 try:
3890 issue = int(options.issue)
3891 except ValueError:
3892 DieWithError('A review issue id is expected to be a number')
3893
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003894 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003895
3896 if options.comment:
3897 cl.AddComment(options.comment)
3898 return 0
3899
3900 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003901 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003902 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003903 summary.append({
3904 'date': message['date'],
3905 'lgtm': False,
3906 'message': message['text'],
3907 'not_lgtm': False,
3908 'sender': message['sender'],
3909 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003910 if message['disapproval']:
3911 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003912 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003913 elif message['approval']:
3914 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003915 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003916 elif message['sender'] == data['owner_email']:
3917 color = Fore.MAGENTA
3918 else:
3919 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003921 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003922 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003923 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003924 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003925 if options.json_file:
3926 with open(options.json_file, 'wb') as f:
3927 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003928 return 0
3929
3930
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003931@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003932def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003933 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003934 parser.add_option('-d', '--display', action='store_true',
3935 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003936 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003937 help='New description to set for this issue (- for stdin, '
3938 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003939 parser.add_option('-f', '--force', action='store_true',
3940 help='Delete any unpublished Gerrit edits for this issue '
3941 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003942
3943 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003944 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003945 options, args = parser.parse_args(args)
3946 _process_codereview_select_options(parser, options)
3947
3948 target_issue = None
3949 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003950 target_issue = ParseIssueNumberArgument(args[0])
3951 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003952 parser.print_help()
3953 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003954
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003955 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003956
martiniss6eda05f2016-06-30 10:18:35 -07003957 kwargs = {
3958 'auth_config': auth_config,
3959 'codereview': options.forced_codereview,
3960 }
3961 if target_issue:
3962 kwargs['issue'] = target_issue.issue
3963 if options.forced_codereview == 'rietveld':
3964 kwargs['rietveld_server'] = target_issue.hostname
3965
3966 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003967
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003968 if not cl.GetIssue():
3969 DieWithError('This branch has no associated changelist.')
3970 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003971
smut@google.com34fb6b12015-07-13 20:03:26 +00003972 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003973 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003974 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003975
3976 if options.new_description:
3977 text = options.new_description
3978 if text == '-':
3979 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003980 elif text == '+':
3981 base_branch = cl.GetCommonAncestorWithUpstream()
3982 change = cl.GetChange(base_branch, None, local_description=True)
3983 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003984
3985 description.set_description(text)
3986 else:
3987 description.prompt()
3988
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003989 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003990 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003991 return 0
3992
3993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994def CreateDescriptionFromLog(args):
3995 """Pulls out the commit log to use as a base for the CL description."""
3996 log_args = []
3997 if len(args) == 1 and not args[0].endswith('.'):
3998 log_args = [args[0] + '..']
3999 elif len(args) == 1 and args[0].endswith('...'):
4000 log_args = [args[0][:-1]]
4001 elif len(args) == 2:
4002 log_args = [args[0] + '..' + args[1]]
4003 else:
4004 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004005 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006
4007
thestig@chromium.org44202a22014-03-11 19:22:18 +00004008def CMDlint(parser, args):
4009 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004010 parser.add_option('--filter', action='append', metavar='-x,+y',
4011 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004012 auth.add_auth_options(parser)
4013 options, args = parser.parse_args(args)
4014 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004015
4016 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004017 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004018 try:
4019 import cpplint
4020 import cpplint_chromium
4021 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004022 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004023 return 1
4024
4025 # Change the current working directory before calling lint so that it
4026 # shows the correct base.
4027 previous_cwd = os.getcwd()
4028 os.chdir(settings.GetRoot())
4029 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004030 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004031 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4032 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004033 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004034 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004035 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004036
4037 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004038 command = args + files
4039 if options.filter:
4040 command = ['--filter=' + ','.join(options.filter)] + command
4041 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004042
4043 white_regex = re.compile(settings.GetLintRegex())
4044 black_regex = re.compile(settings.GetLintIgnoreRegex())
4045 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4046 for filename in filenames:
4047 if white_regex.match(filename):
4048 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004050 else:
4051 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4052 extra_check_functions)
4053 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004055 finally:
4056 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004057 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004058 if cpplint._cpplint_state.error_count != 0:
4059 return 1
4060 return 0
4061
4062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004064 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004065 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004066 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004067 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004068 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004069 auth.add_auth_options(parser)
4070 options, args = parser.parse_args(args)
4071 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072
sbc@chromium.org71437c02015-04-09 19:29:40 +00004073 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004074 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075 return 1
4076
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004077 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 if args:
4079 base_branch = args[0]
4080 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004081 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004082 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004084 cl.RunHook(
4085 committing=not options.upload,
4086 may_prompt=False,
4087 verbose=options.verbose,
4088 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004089 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004090
4091
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004092def GenerateGerritChangeId(message):
4093 """Returns Ixxxxxx...xxx change id.
4094
4095 Works the same way as
4096 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4097 but can be called on demand on all platforms.
4098
4099 The basic idea is to generate git hash of a state of the tree, original commit
4100 message, author/committer info and timestamps.
4101 """
4102 lines = []
4103 tree_hash = RunGitSilent(['write-tree'])
4104 lines.append('tree %s' % tree_hash.strip())
4105 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4106 if code == 0:
4107 lines.append('parent %s' % parent.strip())
4108 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4109 lines.append('author %s' % author.strip())
4110 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4111 lines.append('committer %s' % committer.strip())
4112 lines.append('')
4113 # Note: Gerrit's commit-hook actually cleans message of some lines and
4114 # whitespace. This code is not doing this, but it clearly won't decrease
4115 # entropy.
4116 lines.append(message)
4117 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4118 stdin='\n'.join(lines))
4119 return 'I%s' % change_hash.strip()
4120
4121
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004122def GetTargetRef(remote, remote_branch, target_branch, pending_prefix_check,
4123 remote_url=None):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004124 """Computes the remote branch ref to use for the CL.
4125
4126 Args:
4127 remote (str): The git remote for the CL.
4128 remote_branch (str): The git remote branch for the CL.
4129 target_branch (str): The target branch specified by the user.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004130 pending_prefix_check (bool): If true, determines if pending_prefix should be
4131 used.
4132 remote_url (str): Only used for checking if pending_prefix should be used.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004133 """
4134 if not (remote and remote_branch):
4135 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004136
wittman@chromium.org455dc922015-01-26 20:15:50 +00004137 if target_branch:
4138 # Cannonicalize branch references to the equivalent local full symbolic
4139 # refs, which are then translated into the remote full symbolic refs
4140 # below.
4141 if '/' not in target_branch:
4142 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4143 else:
4144 prefix_replacements = (
4145 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4146 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4147 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4148 )
4149 match = None
4150 for regex, replacement in prefix_replacements:
4151 match = re.search(regex, target_branch)
4152 if match:
4153 remote_branch = target_branch.replace(match.group(0), replacement)
4154 break
4155 if not match:
4156 # This is a branch path but not one we recognize; use as-is.
4157 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004158 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4159 # Handle the refs that need to land in different refs.
4160 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004161
wittman@chromium.org455dc922015-01-26 20:15:50 +00004162 # Create the true path to the remote branch.
4163 # Does the following translation:
4164 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4165 # * refs/remotes/origin/master -> refs/heads/master
4166 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4167 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4168 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4169 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4170 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4171 'refs/heads/')
4172 elif remote_branch.startswith('refs/remotes/branch-heads'):
4173 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004174
4175 if pending_prefix_check:
4176 # If a pending prefix exists then replace refs/ with it.
4177 state = _GitNumbererState.load(remote_url, remote_branch)
4178 if state.pending_prefix:
4179 remote_branch = remote_branch.replace('refs/', state.pending_prefix)
wittman@chromium.org455dc922015-01-26 20:15:50 +00004180 return remote_branch
4181
4182
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004183def cleanup_list(l):
4184 """Fixes a list so that comma separated items are put as individual items.
4185
4186 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4187 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4188 """
4189 items = sum((i.split(',') for i in l), [])
4190 stripped_items = (i.strip() for i in items)
4191 return sorted(filter(None, stripped_items))
4192
4193
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004194@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004195def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004196 """Uploads the current changelist to codereview.
4197
4198 Can skip dependency patchset uploads for a branch by running:
4199 git config branch.branch_name.skip-deps-uploads True
4200 To unset run:
4201 git config --unset branch.branch_name.skip-deps-uploads
4202 Can also set the above globally by using the --global flag.
4203 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004204 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4205 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004206 parser.add_option('--bypass-watchlists', action='store_true',
4207 dest='bypass_watchlists',
4208 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004209 parser.add_option('-f', action='store_true', dest='force',
4210 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004211 parser.add_option('--message', '-m', dest='message',
4212 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004213 parser.add_option('-b', '--bug',
4214 help='pre-populate the bug number(s) for this issue. '
4215 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004216 parser.add_option('--message-file', dest='message_file',
4217 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004218 parser.add_option('--title', '-t', dest='title',
4219 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004220 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004221 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004222 help='reviewer email addresses')
4223 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004224 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004225 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004226 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004227 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004228 parser.add_option('--emulate_svn_auto_props',
4229 '--emulate-svn-auto-props',
4230 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004231 dest="emulate_svn_auto_props",
4232 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004233 parser.add_option('-c', '--use-commit-queue', action='store_true',
4234 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004235 parser.add_option('--private', action='store_true',
4236 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004237 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004238 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004239 metavar='TARGET',
4240 help='Apply CL to remote ref TARGET. ' +
4241 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004242 parser.add_option('--squash', action='store_true',
4243 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004244 parser.add_option('--no-squash', action='store_true',
4245 help='Don\'t squash multiple commits into one ' +
4246 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004247 parser.add_option('--topic', default=None,
4248 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004249 parser.add_option('--email', default=None,
4250 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004251 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4252 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004253 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4254 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004255 help='Send the patchset to do a CQ dry run right after '
4256 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004257 parser.add_option('--dependencies', action='store_true',
4258 help='Uploads CLs of all the local branches that depend on '
4259 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004260
rmistry@google.com2dd99862015-06-22 12:22:18 +00004261 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004262 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004263 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004264 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004265 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004266 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004267 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004268
sbc@chromium.org71437c02015-04-09 19:29:40 +00004269 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004270 return 1
4271
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004272 options.reviewers = cleanup_list(options.reviewers)
4273 options.cc = cleanup_list(options.cc)
4274
tandriib80458a2016-06-23 12:20:07 -07004275 if options.message_file:
4276 if options.message:
4277 parser.error('only one of --message and --message-file allowed.')
4278 options.message = gclient_utils.FileRead(options.message_file)
4279 options.message_file = None
4280
tandrii4d0545a2016-07-06 03:56:49 -07004281 if options.cq_dry_run and options.use_commit_queue:
4282 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4283
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004284 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4285 settings.GetIsGerrit()
4286
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004287 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004288 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004289
4290
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004291def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print()
4293 print('Waiting for commit to be landed on %s...' % real_ref)
4294 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004295 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4296 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004297 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004298
4299 loop = 0
4300 while True:
4301 sys.stdout.write('fetching (%d)... \r' % loop)
4302 sys.stdout.flush()
4303 loop += 1
4304
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004305 if mirror:
4306 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004307 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4308 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4309 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4310 for commit in commits.splitlines():
4311 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004313 return commit
4314
4315 current_rev = to_rev
4316
4317
tandriibf429402016-09-14 07:09:12 -07004318def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004319 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4320
4321 Returns:
4322 (retcode of last operation, output log of last operation).
4323 """
4324 assert pending_ref.startswith('refs/'), pending_ref
4325 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4326 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4327 code = 0
4328 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004329 max_attempts = 3
4330 attempts_left = max_attempts
4331 while attempts_left:
4332 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004334 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004335
4336 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004337 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004338 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004339 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004340 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004341 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004342 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004343 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004344 continue
4345
4346 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004347 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004348 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004349 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004350 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004351 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4352 'the following files have merge conflicts:' % pending_ref)
4353 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4354 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004355 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004356 return code, out
4357
4358 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004359 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004360 code, out = RunGitWithCode(
4361 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4362 if code == 0:
4363 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004365 return code, out
4366
vapiera7fbd5a2016-06-16 09:17:49 -07004367 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004368 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004370 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004371 print('Fatal push error. Make sure your .netrc credentials and git '
4372 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004373 return code, out
4374
vapiera7fbd5a2016-06-16 09:17:49 -07004375 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004376 return code, out
4377
4378
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004379def IsFatalPushFailure(push_stdout):
4380 """True if retrying push won't help."""
4381 return '(prohibited by Gerrit)' in push_stdout
4382
4383
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004384@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004386 """DEPRECATED: Used to commit the current changelist via git-svn."""
4387 message = ('git-cl no longer supports committing to SVN repositories via '
4388 'git-svn. You probably want to use `git cl land` instead.')
4389 print(message)
4390 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391
4392
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004393@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004394def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004395 """Commits the current changelist via git.
4396
4397 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4398 upstream and closes the issue automatically and atomically.
4399
4400 Otherwise (in case of Rietveld):
4401 Squashes branch into a single commit.
4402 Updates commit message with metadata (e.g. pointer to review).
4403 Pushes the code upstream.
4404 Updates review and closes.
4405 """
4406 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4407 help='bypass upload presubmit hook')
4408 parser.add_option('-m', dest='message',
4409 help="override review description")
4410 parser.add_option('-f', action='store_true', dest='force',
4411 help="force yes to questions (don't prompt)")
4412 parser.add_option('-c', dest='contributor',
4413 help="external contributor for patch (appended to " +
4414 "description and used as author for git). Should be " +
4415 "formatted as 'First Last <email@example.com>'")
4416 add_git_similarity(parser)
4417 auth.add_auth_options(parser)
4418 (options, args) = parser.parse_args(args)
4419 auth_config = auth.extract_auth_config_from_options(options)
4420
4421 cl = Changelist(auth_config=auth_config)
4422
4423 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4424 if cl.IsGerrit():
4425 if options.message:
4426 # This could be implemented, but it requires sending a new patch to
4427 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4428 # Besides, Gerrit has the ability to change the commit message on submit
4429 # automatically, thus there is no need to support this option (so far?).
4430 parser.error('-m MESSAGE option is not supported for Gerrit.')
4431 if options.contributor:
4432 parser.error(
4433 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4434 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4435 'the contributor\'s "name <email>". If you can\'t upload such a '
4436 'commit for review, contact your repository admin and request'
4437 '"Forge-Author" permission.')
4438 if not cl.GetIssue():
4439 DieWithError('You must upload the change first to Gerrit.\n'
4440 ' If you would rather have `git cl land` upload '
4441 'automatically for you, see http://crbug.com/642759')
4442 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4443 options.verbose)
4444
4445 current = cl.GetBranch()
4446 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4447 if remote == '.':
4448 print()
4449 print('Attempting to push branch %r into another local branch!' % current)
4450 print()
4451 print('Either reparent this branch on top of origin/master:')
4452 print(' git reparent-branch --root')
4453 print()
4454 print('OR run `git rebase-update` if you think the parent branch is ')
4455 print('already committed.')
4456 print()
4457 print(' Current parent: %r' % upstream_branch)
4458 return 1
4459
4460 if not args:
4461 # Default to merging against our best guess of the upstream branch.
4462 args = [cl.GetUpstreamBranch()]
4463
4464 if options.contributor:
4465 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4466 print("Please provide contibutor as 'First Last <email@example.com>'")
4467 return 1
4468
4469 base_branch = args[0]
4470
4471 if git_common.is_dirty_git_tree('land'):
4472 return 1
4473
4474 # This rev-list syntax means "show all commits not in my branch that
4475 # are in base_branch".
4476 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4477 base_branch]).splitlines()
4478 if upstream_commits:
4479 print('Base branch "%s" has %d commits '
4480 'not in this branch.' % (base_branch, len(upstream_commits)))
4481 print('Run "git merge %s" before attempting to land.' % base_branch)
4482 return 1
4483
4484 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4485 if not options.bypass_hooks:
4486 author = None
4487 if options.contributor:
4488 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4489 hook_results = cl.RunHook(
4490 committing=True,
4491 may_prompt=not options.force,
4492 verbose=options.verbose,
4493 change=cl.GetChange(merge_base, author))
4494 if not hook_results.should_continue():
4495 return 1
4496
4497 # Check the tree status if the tree status URL is set.
4498 status = GetTreeStatus()
4499 if 'closed' == status:
4500 print('The tree is closed. Please wait for it to reopen. Use '
4501 '"git cl land --bypass-hooks" to commit on a closed tree.')
4502 return 1
4503 elif 'unknown' == status:
4504 print('Unable to determine tree status. Please verify manually and '
4505 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4506 return 1
4507
4508 change_desc = ChangeDescription(options.message)
4509 if not change_desc.description and cl.GetIssue():
4510 change_desc = ChangeDescription(cl.GetDescription())
4511
4512 if not change_desc.description:
4513 if not cl.GetIssue() and options.bypass_hooks:
4514 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4515 else:
4516 print('No description set.')
4517 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4518 return 1
4519
4520 # Keep a separate copy for the commit message, because the commit message
4521 # contains the link to the Rietveld issue, while the Rietveld message contains
4522 # the commit viewvc url.
4523 if cl.GetIssue():
4524 change_desc.update_reviewers(cl.GetApprovingReviewers())
4525
4526 commit_desc = ChangeDescription(change_desc.description)
4527 if cl.GetIssue():
4528 # Xcode won't linkify this URL unless there is a non-whitespace character
4529 # after it. Add a period on a new line to circumvent this. Also add a space
4530 # before the period to make sure that Gitiles continues to correctly resolve
4531 # the URL.
4532 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4533 if options.contributor:
4534 commit_desc.append_footer('Patch from %s.' % options.contributor)
4535
4536 print('Description:')
4537 print(commit_desc.description)
4538
4539 branches = [merge_base, cl.GetBranchRef()]
4540 if not options.force:
4541 print_stats(options.similarity, options.find_copies, branches)
4542
4543 # We want to squash all this branch's commits into one commit with the proper
4544 # description. We do this by doing a "reset --soft" to the base branch (which
4545 # keeps the working copy the same), then landing that.
4546 MERGE_BRANCH = 'git-cl-commit'
4547 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4548 # Delete the branches if they exist.
4549 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4550 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4551 result = RunGitWithCode(showref_cmd)
4552 if result[0] == 0:
4553 RunGit(['branch', '-D', branch])
4554
4555 # We might be in a directory that's present in this branch but not in the
4556 # trunk. Move up to the top of the tree so that git commands that expect a
4557 # valid CWD won't fail after we check out the merge branch.
4558 rel_base_path = settings.GetRelativeRoot()
4559 if rel_base_path:
4560 os.chdir(rel_base_path)
4561
4562 # Stuff our change into the merge branch.
4563 # We wrap in a try...finally block so if anything goes wrong,
4564 # we clean up the branches.
4565 retcode = -1
4566 pushed_to_pending = False
4567 pending_ref = None
4568 revision = None
4569 try:
4570 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4571 RunGit(['reset', '--soft', merge_base])
4572 if options.contributor:
4573 RunGit(
4574 [
4575 'commit', '--author', options.contributor,
4576 '-m', commit_desc.description,
4577 ])
4578 else:
4579 RunGit(['commit', '-m', commit_desc.description])
4580
4581 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4582 mirror = settings.GetGitMirror(remote)
4583 if mirror:
4584 pushurl = mirror.url
4585 git_numberer = _GitNumbererState.load(pushurl, branch)
4586 else:
4587 pushurl = remote # Usually, this is 'origin'.
4588 git_numberer = _GitNumbererState.load(
4589 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4590
4591 if git_numberer.should_add_git_number:
4592 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4593 # is no pending ref to push to?
4594 logging.debug('Adding git number footers')
4595 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4596 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4597 branch)
4598 # Ensure timestamps are monotonically increasing.
4599 timestamp = max(1 + _get_committer_timestamp(merge_base),
4600 _get_committer_timestamp('HEAD'))
4601 _git_amend_head(commit_desc.description, timestamp)
4602 change_desc = ChangeDescription(commit_desc.description)
4603 # If gnumbd is sitll ON and we ultimately push to branch with
4604 # pending_prefix, gnumbd will modify footers we've just inserted with
4605 # 'Original-', which is annoying but still technically correct.
4606
4607 pending_prefix = git_numberer.pending_prefix
4608 if not pending_prefix or branch.startswith(pending_prefix):
4609 # If not using refs/pending/heads/* at all, or target ref is already set
4610 # to pending, then push to the target ref directly.
4611 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4612 # in practise. I really tried to create a new branch tracking
4613 # refs/pending/heads/master directly and git cl land failed long before
4614 # reaching this. Disagree? Comment on http://crbug.com/642493.
4615 if pending_prefix:
4616 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4617 'Grab your .git/config, add instructions how to reproduce '
4618 'this, and post it to http://crbug.com/642493.\n'
4619 'The first reporter gets a free "Black Swan" book from '
4620 'tandrii@\n\n')
4621 retcode, output = RunGitWithCode(
4622 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
4623 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
4624 else:
4625 # Cherry-pick the change on top of pending ref and then push it.
4626 assert branch.startswith('refs/'), branch
4627 assert pending_prefix[-1] == '/', pending_prefix
4628 pending_ref = pending_prefix + branch[len('refs/'):]
4629 retcode, output = PushToGitPending(pushurl, pending_ref)
4630 pushed_to_pending = (retcode == 0)
4631
4632 if retcode == 0:
4633 revision = RunGit(['rev-parse', 'HEAD']).strip()
4634 logging.debug(output)
4635 except: # pylint: disable=bare-except
4636 if _IS_BEING_TESTED:
4637 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4638 + '-' * 30 + '8<' + '-' * 30)
4639 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4640 raise
4641 finally:
4642 # And then swap back to the original branch and clean up.
4643 RunGit(['checkout', '-q', cl.GetBranch()])
4644 RunGit(['branch', '-D', MERGE_BRANCH])
4645
4646 if not revision:
4647 print('Failed to push. If this persists, please file a bug.')
4648 return 1
4649
4650 killed = False
4651 if pushed_to_pending:
4652 try:
4653 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4654 # We set pushed_to_pending to False, since it made it all the way to the
4655 # real ref.
4656 pushed_to_pending = False
4657 except KeyboardInterrupt:
4658 killed = True
4659
4660 if cl.GetIssue():
4661 to_pending = ' to pending queue' if pushed_to_pending else ''
4662 viewvc_url = settings.GetViewVCUrl()
4663 if not to_pending:
4664 if viewvc_url and revision:
4665 change_desc.append_footer(
4666 'Committed: %s%s' % (viewvc_url, revision))
4667 elif revision:
4668 change_desc.append_footer('Committed: %s' % (revision,))
4669 print('Closing issue '
4670 '(you may be prompted for your codereview password)...')
4671 cl.UpdateDescription(change_desc.description)
4672 cl.CloseIssue()
4673 props = cl.GetIssueProperties()
4674 patch_num = len(props['patchsets'])
4675 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
4676 patch_num, props['patchsets'][-1], to_pending, revision)
4677 if options.bypass_hooks:
4678 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4679 else:
4680 comment += ' (presubmit successful).'
4681 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4682
4683 if pushed_to_pending:
4684 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4685 print('The commit is in the pending queue (%s).' % pending_ref)
4686 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4687 'footer.' % branch)
4688
4689 if os.path.isfile(POSTUPSTREAM_HOOK):
4690 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4691
4692 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693
4694
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004695@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004697 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698 parser.add_option('-b', dest='newbranch',
4699 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004700 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004702 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4703 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004704 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004705 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004706 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004707 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004708 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004709 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004710
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004711
4712 group = optparse.OptionGroup(
4713 parser,
4714 'Options for continuing work on the current issue uploaded from a '
4715 'different clone (e.g. different machine). Must be used independently '
4716 'from the other options. No issue number should be specified, and the '
4717 'branch must have an issue number associated with it')
4718 group.add_option('--reapply', action='store_true', dest='reapply',
4719 help='Reset the branch and reapply the issue.\n'
4720 'CAUTION: This will undo any local changes in this '
4721 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004722
4723 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004724 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004725 parser.add_option_group(group)
4726
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004727 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004728 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004730 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004731 auth_config = auth.extract_auth_config_from_options(options)
4732
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004733
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004734 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004735 if options.newbranch:
4736 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004737 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004738 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004739
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004740 cl = Changelist(auth_config=auth_config,
4741 codereview=options.forced_codereview)
4742 if not cl.GetIssue():
4743 parser.error('current branch must have an associated issue')
4744
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004745 upstream = cl.GetUpstreamBranch()
4746 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004747 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004748
4749 RunGit(['reset', '--hard', upstream])
4750 if options.pull:
4751 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004752
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004753 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4754 options.directory)
4755
4756 if len(args) != 1 or not args[0]:
4757 parser.error('Must specify issue number or url')
4758
4759 # We don't want uncommitted changes mixed up with the patch.
4760 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004761 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004762
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004763 if options.newbranch:
4764 if options.force:
4765 RunGit(['branch', '-D', options.newbranch],
4766 stderr=subprocess2.PIPE, error_ok=True)
4767 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004768 elif not GetCurrentBranch():
4769 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004770
4771 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4772
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004773 if cl.IsGerrit():
4774 if options.reject:
4775 parser.error('--reject is not supported with Gerrit codereview.')
4776 if options.nocommit:
4777 parser.error('--nocommit is not supported with Gerrit codereview.')
4778 if options.directory:
4779 parser.error('--directory is not supported with Gerrit codereview.')
4780
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004781 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004782 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783
4784
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004785def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004786 """Fetches the tree status and returns either 'open', 'closed',
4787 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004788 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789 if url:
4790 status = urllib2.urlopen(url).read().lower()
4791 if status.find('closed') != -1 or status == '0':
4792 return 'closed'
4793 elif status.find('open') != -1 or status == '1':
4794 return 'open'
4795 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004796 return 'unset'
4797
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004798
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004799def GetTreeStatusReason():
4800 """Fetches the tree status from a json url and returns the message
4801 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004802 url = settings.GetTreeStatusUrl()
4803 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004804 connection = urllib2.urlopen(json_url)
4805 status = json.loads(connection.read())
4806 connection.close()
4807 return status['message']
4808
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004811 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004812 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813 status = GetTreeStatus()
4814 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004815 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004816 return 2
4817
vapiera7fbd5a2016-06-16 09:17:49 -07004818 print('The tree is %s' % status)
4819 print()
4820 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004821 if status != 'open':
4822 return 1
4823 return 0
4824
4825
maruel@chromium.org15192402012-09-06 12:38:29 +00004826def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004827 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004828 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004829 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004830 '-b', '--bot', action='append',
4831 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4832 'times to specify multiple builders. ex: '
4833 '"-b win_rel -b win_layout". See '
4834 'the try server waterfall for the builders name and the tests '
4835 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004836 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004837 '-B', '--bucket', default='',
4838 help=('Buildbucket bucket to send the try requests.'))
4839 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004840 '-m', '--master', default='',
4841 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004842 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004843 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004844 help='Revision to use for the try job; default: the revision will '
4845 'be determined by the try recipe that builder runs, which usually '
4846 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004847 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004848 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004849 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004850 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004851 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004852 '--project',
4853 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004854 'in recipe to determine to which repository or directory to '
4855 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004856 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004857 '-p', '--property', dest='properties', action='append', default=[],
4858 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004859 'key2=value2 etc. The value will be treated as '
4860 'json if decodable, or as string otherwise. '
4861 'NOTE: using this may make your try job not usable for CQ, '
4862 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004863 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004864 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4865 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004866 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004867 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004868 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004869 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004870
machenbach@chromium.org45453142015-09-15 08:45:22 +00004871 # Make sure that all properties are prop=value pairs.
4872 bad_params = [x for x in options.properties if '=' not in x]
4873 if bad_params:
4874 parser.error('Got properties with missing "=": %s' % bad_params)
4875
maruel@chromium.org15192402012-09-06 12:38:29 +00004876 if args:
4877 parser.error('Unknown arguments: %s' % args)
4878
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004879 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004880 if not cl.GetIssue():
4881 parser.error('Need to upload first')
4882
tandriie113dfd2016-10-11 10:20:12 -07004883 error_message = cl.CannotTriggerTryJobReason()
4884 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004885 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004886
borenet6c0efe62016-10-19 08:13:29 -07004887 if options.bucket and options.master:
4888 parser.error('Only one of --bucket and --master may be used.')
4889
qyearsley1fdfcb62016-10-24 13:22:03 -07004890 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004891
qyearsleydd49f942016-10-28 11:57:22 -07004892 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4893 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004894 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004895 if options.verbose:
4896 print('git cl try with no bots now defaults to CQ Dry Run.')
4897 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004898
borenet6c0efe62016-10-19 08:13:29 -07004899 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004900 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004901 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004902 'of bot requires an initial job from a parent (usually a builder). '
4903 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004904 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004905 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004906
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004907 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004908 # TODO(tandrii): Checking local patchset against remote patchset is only
4909 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4910 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004911 print('Warning: Codereview server has newer patchsets (%s) than most '
4912 'recent upload from local checkout (%s). Did a previous upload '
4913 'fail?\n'
4914 'By default, git cl try uses the latest patchset from '
4915 'codereview, continuing to use patchset %s.\n' %
4916 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004917
tandrii568043b2016-10-11 07:49:18 -07004918 try:
borenet6c0efe62016-10-19 08:13:29 -07004919 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4920 patchset)
tandrii568043b2016-10-11 07:49:18 -07004921 except BuildbucketResponseException as ex:
4922 print('ERROR: %s' % ex)
4923 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004924 return 0
4925
4926
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004927def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004928 """Prints info about try jobs associated with current CL."""
4929 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004930 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004931 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004932 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004933 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004934 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004935 '--color', action='store_true', default=setup_color.IS_TTY,
4936 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004937 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004938 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4939 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004940 group.add_option(
4941 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004942 parser.add_option_group(group)
4943 auth.add_auth_options(parser)
4944 options, args = parser.parse_args(args)
4945 if args:
4946 parser.error('Unrecognized args: %s' % ' '.join(args))
4947
4948 auth_config = auth.extract_auth_config_from_options(options)
4949 cl = Changelist(auth_config=auth_config)
4950 if not cl.GetIssue():
4951 parser.error('Need to upload first')
4952
tandrii221ab252016-10-06 08:12:04 -07004953 patchset = options.patchset
4954 if not patchset:
4955 patchset = cl.GetMostRecentPatchset()
4956 if not patchset:
4957 parser.error('Codereview doesn\'t know about issue %s. '
4958 'No access to issue or wrong issue number?\n'
4959 'Either upload first, or pass --patchset explicitely' %
4960 cl.GetIssue())
4961
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004962 # TODO(tandrii): Checking local patchset against remote patchset is only
4963 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4964 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004965 print('Warning: Codereview server has newer patchsets (%s) than most '
4966 'recent upload from local checkout (%s). Did a previous upload '
4967 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004968 'By default, git cl try-results uses the latest patchset from '
4969 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004970 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004971 try:
tandrii221ab252016-10-06 08:12:04 -07004972 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004973 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004974 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004975 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004976 if options.json:
4977 write_try_results_json(options.json, jobs)
4978 else:
4979 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004980 return 0
4981
4982
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004983@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004984def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004985 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004986 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004987 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004988 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004990 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004991 if args:
4992 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004993 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004994 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004995 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004996 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004997
4998 # Clear configured merge-base, if there is one.
4999 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005000 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005001 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005002 return 0
5003
5004
thestig@chromium.org00858c82013-12-02 23:08:03 +00005005def CMDweb(parser, args):
5006 """Opens the current CL in the web browser."""
5007 _, args = parser.parse_args(args)
5008 if args:
5009 parser.error('Unrecognized args: %s' % ' '.join(args))
5010
5011 issue_url = Changelist().GetIssueURL()
5012 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005013 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005014 return 1
5015
5016 webbrowser.open(issue_url)
5017 return 0
5018
5019
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005020def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005021 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005022 parser.add_option('-d', '--dry-run', action='store_true',
5023 help='trigger in dry run mode')
5024 parser.add_option('-c', '--clear', action='store_true',
5025 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005026 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005027 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005028 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005029 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005030 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005031 if args:
5032 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005033 if options.dry_run and options.clear:
5034 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5035
iannuccie53c9352016-08-17 14:40:40 -07005036 cl = Changelist(auth_config=auth_config, issue=options.issue,
5037 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005038 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005039 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005040 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005041 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005042 state = _CQState.DRY_RUN
5043 else:
5044 state = _CQState.COMMIT
5045 if not cl.GetIssue():
5046 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005047 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005048 return 0
5049
5050
groby@chromium.org411034a2013-02-26 15:12:01 +00005051def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005052 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005053 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005054 auth.add_auth_options(parser)
5055 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005056 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005057 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005058 if args:
5059 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005060 cl = Changelist(auth_config=auth_config, issue=options.issue,
5061 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005062 # Ensure there actually is an issue to close.
5063 cl.GetDescription()
5064 cl.CloseIssue()
5065 return 0
5066
5067
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005068def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005069 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005070 parser.add_option(
5071 '--stat',
5072 action='store_true',
5073 dest='stat',
5074 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005075 auth.add_auth_options(parser)
5076 options, args = parser.parse_args(args)
5077 auth_config = auth.extract_auth_config_from_options(options)
5078 if args:
5079 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005080
5081 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005082 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005083 # Staged changes would be committed along with the patch from last
5084 # upload, hence counted toward the "last upload" side in the final
5085 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005086 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005087 return 1
5088
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005089 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005090 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005091 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005092 if not issue:
5093 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005094 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005095 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005096
5097 # Create a new branch based on the merge-base
5098 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005099 # Clear cached branch in cl object, to avoid overwriting original CL branch
5100 # properties.
5101 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005102 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005103 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005104 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005105 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005106 return rtn
5107
wychen@chromium.org06928532015-02-03 02:11:29 +00005108 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005109 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005110 cmd = ['git', 'diff']
5111 if options.stat:
5112 cmd.append('--stat')
5113 cmd.extend([TMP_BRANCH, branch, '--'])
5114 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005115 finally:
5116 RunGit(['checkout', '-q', branch])
5117 RunGit(['branch', '-D', TMP_BRANCH])
5118
5119 return 0
5120
5121
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005122def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005123 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005124 parser.add_option(
5125 '--no-color',
5126 action='store_true',
5127 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005128 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005129 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005130 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005131
5132 author = RunGit(['config', 'user.email']).strip() or None
5133
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005134 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005135
5136 if args:
5137 if len(args) > 1:
5138 parser.error('Unknown args')
5139 base_branch = args[0]
5140 else:
5141 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005142 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005143
5144 change = cl.GetChange(base_branch, None)
5145 return owners_finder.OwnersFinder(
5146 [f.LocalPath() for f in
5147 cl.GetChange(base_branch, None).AffectedFiles()],
5148 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005149 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005150 disable_color=options.no_color).run()
5151
5152
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005153def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005154 """Generates a diff command."""
5155 # Generate diff for the current branch's changes.
5156 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5157 upstream_commit, '--' ]
5158
5159 if args:
5160 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005161 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005162 diff_cmd.append(arg)
5163 else:
5164 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005165
5166 return diff_cmd
5167
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005168def MatchingFileType(file_name, extensions):
5169 """Returns true if the file name ends with one of the given extensions."""
5170 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005171
enne@chromium.org555cfe42014-01-29 18:21:39 +00005172@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005173def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005174 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lam06dba1b2017-01-18 16:39:43 +11005175 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java', '.js']
kylechar58edce22016-06-17 06:07:51 -07005176 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005177 parser.add_option('--full', action='store_true',
5178 help='Reformat the full content of all touched files')
5179 parser.add_option('--dry-run', action='store_true',
5180 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005181 parser.add_option('--python', action='store_true',
5182 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005183 parser.add_option('--diff', action='store_true',
5184 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005185 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005186
Daniel Chengc55eecf2016-12-30 03:11:02 -08005187 # Normalize any remaining args against the current path, so paths relative to
5188 # the current directory are still resolved as expected.
5189 args = [os.path.join(os.getcwd(), arg) for arg in args]
5190
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005191 # git diff generates paths against the root of the repository. Change
5192 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005193 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005194 if rel_base_path:
5195 os.chdir(rel_base_path)
5196
digit@chromium.org29e47272013-05-17 17:01:46 +00005197 # Grab the merge-base commit, i.e. the upstream commit of the current
5198 # branch when it was created or the last time it was rebased. This is
5199 # to cover the case where the user may have called "git fetch origin",
5200 # moving the origin branch to a newer commit, but hasn't rebased yet.
5201 upstream_commit = None
5202 cl = Changelist()
5203 upstream_branch = cl.GetUpstreamBranch()
5204 if upstream_branch:
5205 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5206 upstream_commit = upstream_commit.strip()
5207
5208 if not upstream_commit:
5209 DieWithError('Could not find base commit for this branch. '
5210 'Are you in detached state?')
5211
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005212 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5213 diff_output = RunGit(changed_files_cmd)
5214 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005215 # Filter out files deleted by this CL
5216 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005217
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005218 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5219 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5220 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005221 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005222
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005223 top_dir = os.path.normpath(
5224 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5225
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005226 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5227 # formatted. This is used to block during the presubmit.
5228 return_value = 0
5229
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005230 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005231 # Locate the clang-format binary in the checkout
5232 try:
5233 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005234 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005235 DieWithError(e)
5236
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005237 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005238 cmd = [clang_format_tool]
5239 if not opts.dry_run and not opts.diff:
5240 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005241 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005242 if opts.diff:
5243 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005244 else:
5245 env = os.environ.copy()
5246 env['PATH'] = str(os.path.dirname(clang_format_tool))
5247 try:
5248 script = clang_format.FindClangFormatScriptInChromiumTree(
5249 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005250 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005251 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005252
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005253 cmd = [sys.executable, script, '-p0']
5254 if not opts.dry_run and not opts.diff:
5255 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005256
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005257 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5258 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005259
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005260 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5261 if opts.diff:
5262 sys.stdout.write(stdout)
5263 if opts.dry_run and len(stdout) > 0:
5264 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005265
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005266 # Similar code to above, but using yapf on .py files rather than clang-format
5267 # on C/C++ files
5268 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005269 yapf_tool = gclient_utils.FindExecutable('yapf')
5270 if yapf_tool is None:
5271 DieWithError('yapf not found in PATH')
5272
5273 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005274 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005275 cmd = [yapf_tool]
5276 if not opts.dry_run and not opts.diff:
5277 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005278 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005279 if opts.diff:
5280 sys.stdout.write(stdout)
5281 else:
5282 # TODO(sbc): yapf --lines mode still has some issues.
5283 # https://github.com/google/yapf/issues/154
5284 DieWithError('--python currently only works with --full')
5285
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005286 # Dart's formatter does not have the nice property of only operating on
5287 # modified chunks, so hard code full.
5288 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005289 try:
5290 command = [dart_format.FindDartFmtToolInChromiumTree()]
5291 if not opts.dry_run and not opts.diff:
5292 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005293 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005294
ppi@chromium.org6593d932016-03-03 15:41:15 +00005295 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005296 if opts.dry_run and stdout:
5297 return_value = 2
5298 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005299 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5300 'found in this checkout. Files in other languages are still '
5301 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005302
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005303 # Format GN build files. Always run on full build files for canonical form.
5304 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005305 cmd = ['gn', 'format' ]
5306 if opts.dry_run or opts.diff:
5307 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005308 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005309 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5310 shell=sys.platform == 'win32',
5311 cwd=top_dir)
5312 if opts.dry_run and gn_ret == 2:
5313 return_value = 2 # Not formatted.
5314 elif opts.diff and gn_ret == 2:
5315 # TODO this should compute and print the actual diff.
5316 print("This change has GN build file diff for " + gn_diff_file)
5317 elif gn_ret != 0:
5318 # For non-dry run cases (and non-2 return values for dry-run), a
5319 # nonzero error code indicates a failure, probably because the file
5320 # doesn't parse.
5321 DieWithError("gn format failed on " + gn_diff_file +
5322 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005323
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005324 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005325
5326
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327@subcommand.usage('<codereview url or issue id>')
5328def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005329 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005330 _, args = parser.parse_args(args)
5331
5332 if len(args) != 1:
5333 parser.print_help()
5334 return 1
5335
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005336 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005337 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005338 parser.print_help()
5339 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005340 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005341
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005342 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005343 output = RunGit(['config', '--local', '--get-regexp',
5344 r'branch\..*\.%s' % issueprefix],
5345 error_ok=True)
5346 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005347 if issue == target_issue:
5348 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005349
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005350 branches = []
5351 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005352 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005353 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005354 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005355 return 1
5356 if len(branches) == 1:
5357 RunGit(['checkout', branches[0]])
5358 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005359 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005360 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005362 which = raw_input('Choose by index: ')
5363 try:
5364 RunGit(['checkout', branches[int(which)]])
5365 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005367 return 1
5368
5369 return 0
5370
5371
maruel@chromium.org29404b52014-09-08 22:58:00 +00005372def CMDlol(parser, args):
5373 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005374 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005375 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5376 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5377 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005378 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005379 return 0
5380
5381
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005382class OptionParser(optparse.OptionParser):
5383 """Creates the option parse and add --verbose support."""
5384 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005385 optparse.OptionParser.__init__(
5386 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005387 self.add_option(
5388 '-v', '--verbose', action='count', default=0,
5389 help='Use 2 times for more debugging info')
5390
5391 def parse_args(self, args=None, values=None):
5392 options, args = optparse.OptionParser.parse_args(self, args, values)
5393 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005394 logging.basicConfig(
5395 level=levels[min(options.verbose, len(levels) - 1)],
5396 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5397 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005398 return options, args
5399
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005401def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005402 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005403 print('\nYour python version %s is unsupported, please upgrade.\n' %
5404 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005405 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005406
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005407 # Reload settings.
5408 global settings
5409 settings = Settings()
5410
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005411 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005412 dispatcher = subcommand.CommandDispatcher(__name__)
5413 try:
5414 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005415 except auth.AuthenticationError as e:
5416 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005417 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005418 if e.code != 500:
5419 raise
5420 DieWithError(
5421 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5422 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005423 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005424
5425
5426if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005427 # These affect sys.stdout so do it outside of main() to simplify mocks in
5428 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005429 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005430 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005431 try:
5432 sys.exit(main(sys.argv[1:]))
5433 except KeyboardInterrupt:
5434 sys.stderr.write('interrupted\n')
5435 sys.exit(1)